@eggjs/router
Version:
Router middleware for egg/koa. Provides RESTful resource routing.
352 lines (331 loc) • 13.9 kB
text/typescript
import assert from 'node:assert';
import { encodeURIComponent as safeEncodeURIComponent } from 'utility';
import inflection from 'inflection';
import methods from 'methods';
import { isGeneratorFunction } from 'is-type-of';
import { RegisterOptions, Router, RouterMethod, RouterOptions } from './Router.js';
import { MiddlewareFunc, Next, ResourcesController } from './types.js';
interface RestfulOptions {
suffix?: string;
namePrefix?: string;
method: string | string[];
member?: true;
}
const REST_MAP: Record<string, RestfulOptions> = {
index: {
suffix: '',
method: 'GET',
},
new: {
namePrefix: 'new_',
member: true,
suffix: 'new',
method: 'GET',
},
create: {
suffix: '',
method: 'POST',
},
show: {
member: true,
suffix: ':id',
method: 'GET',
},
edit: {
member: true,
namePrefix: 'edit_',
suffix: ':id/edit',
method: 'GET',
},
update: {
member: true,
namePrefix: '',
suffix: ':id',
method: [ 'PATCH', 'PUT' ],
},
destroy: {
member: true,
namePrefix: 'destroy_',
suffix: ':id',
method: 'DELETE',
},
};
interface Application {
controller: Record<string, any>;
}
/**
* FIXME: move these patch into @eggjs/router
*/
export class EggRouter extends Router {
readonly app: Application;
/**
* @class
* @param {Object} opts - Router options.
* @param {Application} app - Application object.
*/
constructor(opts: RouterOptions, app: Application) {
super(opts);
this.app = app;
}
verb(method: RouterMethod | RouterMethod[],
nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middleware: (MiddlewareFunc | string)[]) {
const { path, middlewares, options } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
if (typeof method === 'string') {
method = [ method ];
}
this.register(path, method, middlewares, options);
return this;
}
// const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all' ];
head(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
head(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
head(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares);
}
options(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
options(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
options(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares);
}
get(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
get(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
get(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares);
}
put(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
put(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
put(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares);
}
patch(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
patch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
patch(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares);
}
post(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
post(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
post(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares);
}
delete(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
delete(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
delete(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares);
}
all(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
all(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router;
all(nameOrPath: string | RegExp | (string | RegExp)[],
pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc,
...middlewares: (MiddlewareFunc | string)[]): Router {
return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares);
}
register(path: string | RegExp | (string | RegExp)[],
methods: string[],
middleware: MiddlewareFunc | string | (MiddlewareFunc | string | ResourcesController)[],
opts?: RegisterOptions) {
// patch register to support bind ctx function middleware and string controller
middleware = Array.isArray(middleware) ? middleware : [ middleware ];
for (const mw of middleware) {
if (isGeneratorFunction(mw)) {
throw new TypeError(
methods.toString() + ' `' + path + '`: Please use async function instead of generator function',
);
}
}
const middlewares = convertMiddlewares(middleware, this.app);
return super.register(path, methods, middlewares, opts);
}
/**
* restful router api
* @param {String} name - Router name
* @param {String} prefix - url prefix
* @param {Function} middleware - middleware or controller
* @example
* ```js
* app.resources('/posts', 'posts')
* app.resources('posts', '/posts', 'posts')
* app.resources('posts', '/posts', app.role.can('user'), app.controller.posts)
* app.resources('posts', '/posts', middleware1, middleware2, app.controller.posts)
* ```
*
* Examples:
*
* ```js
* app.resources('/posts', 'posts')
* ```
*
* yield router mapping
*
* Method | Path | Route Name | Controller.Action
* -------|-----------------|----------------|-----------------------------
* GET | /posts | posts | app.controller.posts.index
* GET | /posts/new | new_post | app.controller.posts.new
* GET | /posts/:id | post | app.controller.posts.show
* GET | /posts/:id/edit | edit_post | app.controller.posts.edit
* POST | /posts | posts | app.controller.posts.create
* PATCH | /posts/:id | post | app.controller.posts.update
* DELETE | /posts/:id | post | app.controller.posts.destroy
*
* app.router.url can generate url based on arguments
* ```js
* app.router.url('posts')
* => /posts
* app.router.url('post', { id: 1 })
* => /posts/1
* app.router.url('new_post')
* => /posts/new
* app.router.url('edit_post', { id: 1 })
* => /posts/1/edit
* ```
* @return {Router} return route object.
* @since 1.0.0
*/
resources(prefix: string, controller: string | ResourcesController): Router;
resources(prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router;
resources(name: string, prefix: string, controller: string | ResourcesController): Router;
resources(name: string, prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router;
resources(nameOrPath: string | RegExp, ...middleware: (MiddlewareFunc | string | ResourcesController)[]): Router;
resources(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController,
...middleware: (MiddlewareFunc | string | ResourcesController)[]): Router {
const { path, middlewares, options } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware);
// last argument is Controller object
const controller = resolveController(middlewares.pop()!, this.app);
for (const key in REST_MAP) {
const action = controller[key] as MiddlewareFunc;
if (!action) continue;
const opts = REST_MAP[key];
let routeName;
if (opts.member) {
routeName = inflection.singularize(options.name ?? '');
} else {
routeName = inflection.pluralize(options.name ?? '');
}
if (opts.namePrefix) {
routeName = opts.namePrefix + routeName;
}
const prefix = (path as string).replace(/\/$/, '');
const urlPath = opts.suffix ? `${prefix}/${opts.suffix}` : prefix;
const method = Array.isArray(opts.method) ? opts.method : [ opts.method ];
this.register(urlPath, method, middlewares.concat(action), { name: routeName });
}
return this;
}
/**
* @param {String} name - Router name
* @param {Object} params - more parameters
* @example
* ```js
* router.url('edit_post', { id: 1, name: 'foo', page: 2 })
* => /posts/1/edit?name=foo&page=2
* router.url('posts', { name: 'foo&1', page: 2 })
* => /posts?name=foo%261&page=2
* ```
* @return {String} url by path name and query params.
* @since 1.0.0
*/
url(name: string, params?: Record<string, string | number | (string | number)[]>): string {
const route = this.route(name);
if (!route) return '';
const args = params;
let url = route.path;
assert(!(url instanceof RegExp), `Can't get the url for regExp ${url} for by name '${name}'`);
const queries = [];
if (typeof args === 'object' && args !== null) {
const replacedParams: string[] = [];
url = url.replace(/:([a-zA-Z_]\w*)/g, ($0, key) => {
if (key in args) {
const values = args[key];
replacedParams.push(key);
return safeEncodeURIComponent(Array.isArray(values) ? String(values[0]) : String(values));
}
return $0;
});
for (const key in args) {
if (replacedParams.includes(key)) {
continue;
}
const values = args[key];
const encodedKey = safeEncodeURIComponent(key);
if (Array.isArray(values)) {
for (const val of values) {
queries.push(`${encodedKey}=${safeEncodeURIComponent(String(val))}`);
}
} else {
queries.push(`${encodedKey}=${safeEncodeURIComponent(String(values))}`);
}
}
}
if (queries.length > 0) {
const queryStr = queries.join('&');
if (!url.includes('?')) {
url = `${url}?${queryStr}`;
} else {
url = `${url}&${queryStr}`;
}
}
return url;
}
/**
* @alias to url()
*/
pathFor(name: string, params?: Record<string, string | number | (string | number)[]>) {
return this.url(name, params);
}
}
/**
* resolve controller from string to function
* @param {String|Function} controller input controller
* @param {Application} app egg application instance
*/
function resolveController(controller: string | MiddlewareFunc | ResourcesController, app: Application) {
if (typeof controller === 'string') {
// resolveController('foo.bar.Home', app)
const actions = controller.split('.');
let obj = app.controller;
actions.forEach(key => {
obj = obj[key];
if (!obj) throw new Error(`app.controller.${controller} not exists`);
});
controller = obj as any;
}
// ensure controller is exists
if (!controller) throw new Error('controller not exists');
return controller as any;
}
/**
* 1. ensure controller(last argument) support string
* - [url, controller]: app.get('/home', 'home');
* - [name, url, controller(string)]: app.get('posts', '/posts', 'posts.list');
* - [name, url, controller]: app.get('posts', '/posts', app.controller.posts.list);
* - [name, url(regexp), controller]: app.get('regRouter', /\/home\/index/, 'home.index');
* - [name, url, middleware, [...], controller]: `app.get(/user/:id', hasLogin, canGetUser, 'user.show');`
*
* 2. bind ctx to controller `this`
*
* @param {Array} middlewares middlewares and controller(last middleware)
* @param {Application} app egg application instance
*/
function convertMiddlewares(middlewares: (MiddlewareFunc | string | ResourcesController)[], app: Application) {
// ensure controller is resolved
const controller = resolveController(middlewares.pop()!, app);
function wrappedController(ctx: any, next: Next) {
return controller.apply(ctx, [ ctx, next ]);
}
return [ ...middlewares as MiddlewareFunc[], wrappedController ];
}