@midwayjs/koa
Version:
Midway Web Framework for KOA
367 lines • 15.6 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MidwayKoaFramework = void 0;
const core_1 = require("@midwayjs/core");
const cookies_1 = require("@midwayjs/cookies");
const Router = require("@koa/router");
const koa = require("koa");
const onerror_1 = require("./onerror");
const qs = require("qs");
const querystring = require("querystring");
const utils_1 = require("./utils");
const COOKIES = Symbol('context#cookies');
class KoaControllerGenerator extends core_1.WebControllerGenerator {
app;
webRouterService;
constructor(app, webRouterService) {
super(app, webRouterService);
this.app = app;
this.webRouterService = webRouterService;
}
createRouter(routerOptions) {
const router = new Router(routerOptions);
router.prefix(routerOptions.prefix);
return router;
}
generateController(routeInfo) {
return this.generateKoaController(routeInfo);
}
}
let MidwayKoaFramework = class MidwayKoaFramework extends core_1.BaseFramework {
server;
generator;
webRouterService;
configure() {
return this.configService.getConfiguration('koa');
}
async applicationInitialize(options) {
const appKeys = this.configService.getConfiguration('keys') ||
this.configurationOptions['keys'];
if (!appKeys) {
throw new core_1.MidwayConfigMissingError('config.keys');
}
const cookieOptions = this.configService.getConfiguration('cookies');
const cookieGetOptions = this.configService.getConfiguration('cookiesExtra.defaultGetOptions');
this.app = new koa({
keys: [].concat(appKeys),
proxy: this.configurationOptions.proxy,
proxyIpHeader: this.configurationOptions.proxyIpHeader,
subdomainOffset: this.configurationOptions.subdomainOffset,
maxIpsCount: this.configurationOptions.maxIpsCount,
});
Object.defineProperty(this.app.context, 'cookies', {
get() {
if (!this[COOKIES]) {
this[COOKIES] = new cookies_1.Cookies(this, this.app.keys, cookieOptions, cookieGetOptions);
}
return this[COOKIES];
},
enumerable: true,
});
Object.defineProperty(this.app.context, 'locals', {
get() {
return this.state;
},
set(value) {
this.state = value;
},
});
Object.defineProperty(this.app.context, 'forward', {
get() {
return async function (url) {
const routerService = this.requestContext.get(core_1.MidwayWebRouterService);
const matchedUrlRouteInfo = await routerService.getMatchedRouterInfo(url, this.method);
if (matchedUrlRouteInfo) {
if (matchedUrlRouteInfo.controllerClz) {
// normal class controller router
const controllerInstance = await this.requestContext.getAsync(matchedUrlRouteInfo.controllerClz);
return controllerInstance[matchedUrlRouteInfo.method](this);
}
else if (typeof matchedUrlRouteInfo.method === 'function') {
// dynamic router
return matchedUrlRouteInfo.method(this);
}
}
else {
throw new core_1.httpError.NotFoundError(`Forward url ${url} Not Found`);
}
};
},
});
const converter = this.configurationOptions.queryParseMode === 'strict'
? function (value) {
return !Array.isArray(value) ? [value] : value;
}
: this.configurationOptions.queryParseMode === 'first'
? function (value) {
return Array.isArray(value) ? value[0] : value;
}
: undefined;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
// fix query with array params
Object.defineProperty(this.app.request, 'query', {
get() {
const str = this.querystring;
const c = (this._querycache = this._querycache || {});
// find cache
if (c[str])
return c[str];
if (self.configurationOptions.queryParseMode) {
// use qs module to parse query
const parseOptions = {
...self.configurationOptions.queryParseOptions,
...(self.configurationOptions.queryParseMode === 'first'
? { duplicates: 'first' }
: {}),
};
c[str] = qs.parse(str, parseOptions);
}
else {
// use querystring to parse query by default
c[str] = querystring.parse(str);
}
if (converter) {
for (const key in c[str]) {
c[str][key] = converter(c[str][key]);
}
}
return c[str];
},
set(value) {
this._querycache = this._querycache || {};
this._querycache[this.querystring] = value;
},
});
const onerrorConfig = this.configService.getConfiguration('onerror');
(0, onerror_1.setupOnError)(this.app, onerrorConfig, this.logger);
// not found middleware
const notFound = async (ctx, next) => {
await next();
if (!ctx._matchedRoute && ctx.body === undefined) {
throw new core_1.httpError.NotFoundError(`${ctx.path} Not Found`);
}
};
const applyMiddlewares = [notFound];
// versioning middleware
const versioningConfig = this.configurationOptions.versioning;
if (versioningConfig?.enabled) {
applyMiddlewares.push(this.createVersioningMiddleware(versioningConfig));
}
// root middleware
const midwayRootMiddleware = async (ctx, next) => {
this.app.createAnonymousContext(ctx);
const traceService = this.applicationContext.get(core_1.MidwayTraceService);
const spanName = `${ctx.method} ${ctx.path || '/'}`;
const traceMetaResolver = this.configurationOptions?.tracing
?.meta;
const traceEnabled = this.configurationOptions?.tracing?.enable !== false;
const traceExtractor = this.configurationOptions?.tracing
?.extractor;
const traceInjector = this.configurationOptions?.tracing
?.injector;
const requestCarrier = typeof traceExtractor === 'function'
? traceExtractor({
ctx,
request: ctx.request,
response: ctx.response,
})
: ctx.headers;
const responseCarrier = typeof traceInjector === 'function'
? traceInjector({ ctx, request: ctx.request, response: ctx.response })
: ctx.response.res;
await traceService.runWithEntrySpan(spanName, {
enable: traceEnabled,
carrier: requestCarrier,
responseCarrier,
attributes: {
'midway.protocol': 'http',
},
meta: traceMetaResolver,
metaArgs: {
ctx,
carrier: requestCarrier,
request: ctx.request,
response: ctx.response,
},
}, async () => {
await (await this.applyMiddleware(applyMiddlewares))(ctx, next);
if (ctx.body === undefined &&
!ctx.response._explicitStatus &&
ctx._matchedRoute) {
// 如果进了路由,重新赋值,防止 404
ctx.body = undefined;
}
});
};
this.app.use(midwayRootMiddleware);
this.webRouterService = await this.applicationContext.getAsync(core_1.MidwayWebRouterService, [
{
globalPrefix: this.configurationOptions.globalPrefix,
},
]);
this.generator = new KoaControllerGenerator(this.app, this.webRouterService);
this.defineApplicationProperties({
generateController: this.generateController.bind(this),
getPort: this.getPort.bind(this),
});
// hack use method
this.app.originUse = this.app.use;
this.app.use = this.app.useMiddleware;
}
async loadMidwayController() {
await this.generator.loadMidwayController(newRouter => {
const dispatchFn = newRouter.middleware();
dispatchFn._name = `midwayController(${newRouter?.opts?.prefix || '/'})`;
this.app.use(dispatchFn);
});
}
/**
* wrap controller string to middleware function
*/
generateController(routeInfo) {
return this.generator.generateKoaController(routeInfo);
}
async run() {
// load controller
await this.loadMidwayController();
// restore use method
this.app.use = this.app.originUse;
const serverOptions = {
...this.configurationOptions,
...this.configurationOptions.serverOptions,
};
// https config
if (serverOptions.key && serverOptions.cert) {
serverOptions.key = core_1.PathFileUtils.getFileContentSync(serverOptions.key);
serverOptions.cert = core_1.PathFileUtils.getFileContentSync(serverOptions.cert);
serverOptions.ca = core_1.PathFileUtils.getFileContentSync(serverOptions.ca);
process.env.MIDWAY_HTTP_SSL = 'true';
if (serverOptions.http2) {
this.server = require('http2').createSecureServer(serverOptions, this.app.callback());
}
else {
this.server = require('https').createServer(serverOptions, this.app.callback());
}
}
else {
if (serverOptions.http2) {
this.server = require('http2').createServer(serverOptions, this.app.callback());
}
else {
this.server = require('http').createServer(serverOptions, this.app.callback());
}
}
// register httpServer to applicationContext
this.applicationContext.registerObject(core_1.HTTP_SERVER_KEY, this.server);
// server timeout
if (core_1.Types.isNumber(this.configurationOptions.serverTimeout)) {
this.server.setTimeout(this.configurationOptions.serverTimeout);
}
this.configurationOptions.listenOptions = {
port: this.configurationOptions.port,
host: this.configurationOptions.hostname,
...this.configurationOptions.listenOptions,
};
// set port and listen server
let customPort = process.env.MIDWAY_HTTP_PORT ||
this.configurationOptions.listenOptions.port;
if (customPort === 0 || customPort === '0') {
customPort = await (0, utils_1.getFreePort)();
this.logger.info(`[midway:koa] server has auto-assigned port ${customPort}`);
}
this.configurationOptions.listenOptions.port = Number(customPort);
if (this.configurationOptions.listenOptions.port) {
new Promise(resolve => {
// 使用 ListenOptions 对象启动服务器
this.server.listen(this.configurationOptions.listenOptions, () => {
resolve();
});
// 设置环境变量
process.env.MIDWAY_HTTP_PORT = String(this.configurationOptions.listenOptions.port);
});
this.logger.debug(`[midway:koa] server is listening on port ${customPort}`);
}
}
async beforeStop() {
if (this.server) {
new Promise(resolve => {
this.server.close(resolve);
process.env.MIDWAY_HTTP_PORT = '';
});
this.logger.debug('[midway:koa] server is stopped!');
}
}
getFrameworkName() {
return 'koa';
}
getServer() {
return this.server;
}
getPort() {
return process.env.MIDWAY_HTTP_PORT;
}
useMiddleware(Middleware) {
this.middlewareManager.insertLast(Middleware);
}
useFilter(Filter) {
this.filterManager.useFilter(Filter);
}
createVersioningMiddleware(config) {
return async (ctx, next) => {
// 提取版本信息
const version = this.extractVersion(ctx, config);
ctx.apiVersion = version;
// 对于 URI 版本控制,重写路径
if (config.type === 'URI' && version) {
const versionPrefix = `/${config.prefix || 'v'}${version}`;
if (ctx.path.startsWith(versionPrefix)) {
ctx.originalPath = ctx.path;
ctx.path = ctx.path.replace(versionPrefix, '') || '/';
}
}
await next();
};
}
extractVersion(ctx, config) {
// 自定义提取函数优先
if (config.extractVersionFn) {
return config.extractVersionFn(ctx);
}
const type = config.type || 'URI';
switch (type) {
case 'HEADER': {
const headerName = config.header || 'x-api-version';
const headerValue = ctx.headers[headerName];
if (typeof headerValue === 'string') {
return headerValue.replace(/^v/, '');
}
return undefined;
}
case 'MEDIA_TYPE': {
const accept = ctx.headers.accept;
const paramName = config.mediaTypeParam || 'version';
const match = accept?.match(new RegExp(`${paramName}=(\\d+)`));
return match ? match[1] : undefined;
}
case 'URI': {
const prefix = config.prefix || 'v';
const uriMatch = ctx.path.match(new RegExp(`^/${prefix}(\\d+)`));
return uriMatch ? uriMatch[1] : undefined;
}
default:
return config.defaultVersion;
}
}
};
exports.MidwayKoaFramework = MidwayKoaFramework;
exports.MidwayKoaFramework = MidwayKoaFramework = __decorate([
(0, core_1.Framework)()
], MidwayKoaFramework);
//# sourceMappingURL=framework.js.map