Skip to content

Commit f72339c

Browse files
authored
feat: support route version for http (#4407)
* fix: special in filename * fix: middleware pos * fix: middleware pos * fix: middleware pos * fix: middleware pos
1 parent 614fb0b commit f72339c

File tree

27 files changed

+2706
-1416
lines changed

27 files changed

+2706
-1416
lines changed

packages/core/src/decorator/web/controller.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export interface ControllerOption {
1111
description?: string;
1212
tagName?: string;
1313
ignoreGlobalPrefix?: boolean;
14+
// 版本控制配置
15+
version?: string | string[];
16+
versionType?: 'URI' | 'HEADER' | 'MEDIA_TYPE' | 'CUSTOM';
17+
versionPrefix?: string;
1418
};
1519
}
1620

@@ -22,6 +26,10 @@ export function Controller(
2226
description?: string;
2327
tagName?: string;
2428
ignoreGlobalPrefix?: boolean;
29+
// 版本控制配置
30+
version?: string | string[];
31+
versionType?: 'URI' | 'HEADER' | 'MEDIA_TYPE' | 'CUSTOM';
32+
versionPrefix?: string;
2533
} = { middleware: [], sensitive: true }
2634
): ClassDecorator {
2735
return (target: any) => {

packages/core/src/service/webRouterService.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ export interface RouterInfo {
119119
* url after wildcard and can be path-to-regexp by path-to-regexp v6
120120
*/
121121
fullUrlFlattenString?: string;
122+
123+
/**
124+
* version information for API versioning
125+
*/
126+
version?: string | string[];
127+
128+
/**
129+
* version type for API versioning
130+
*/
131+
versionType?: 'URI' | 'HEADER' | 'MEDIA_TYPE' | 'CUSTOM';
132+
133+
/**
134+
* version prefix for URI versioning
135+
*/
136+
versionPrefix?: string;
122137
}
123138

124139
export type DynamicRouterInfo = Omit<
@@ -248,11 +263,37 @@ export class MidwayWebRouterService {
248263
controllerOption.prefix || '/'
249264
);
250265
const ignorePrefix = controllerOption.prefix || '/';
266+
251267
// if controller set ignore global prefix, all router will be ignore too.
252268
if (controllerIgnoreGlobalPrefix) {
253269
prefix = ignorePrefix;
254270
}
255271

272+
// Apply version prefix for URI versioning
273+
if (
274+
controllerOption.routerOptions?.version &&
275+
(!controllerOption.routerOptions?.versionType ||
276+
controllerOption.routerOptions?.versionType === 'URI')
277+
) {
278+
const versionPrefix =
279+
controllerOption.routerOptions?.versionPrefix || 'v';
280+
const version = Array.isArray(controllerOption.routerOptions.version)
281+
? controllerOption.routerOptions.version[0]
282+
: controllerOption.routerOptions.version;
283+
284+
const versionedPrefix = `/${versionPrefix}${version}`;
285+
286+
if (controllerIgnoreGlobalPrefix) {
287+
prefix = joinURLPath(versionedPrefix, ignorePrefix);
288+
} else {
289+
prefix = joinURLPath(
290+
this.options.globalPrefix,
291+
versionedPrefix,
292+
controllerOption.prefix || '/'
293+
);
294+
}
295+
}
296+
256297
if (/\*/.test(prefix)) {
257298
throw new MidwayCommonError(
258299
`Router prefix ${prefix} can't set string with *`

packages/mcp/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
export * from './configuration';
1+
export { MCPConfiguration as Configuration } from './configuration';
22
export * from './framework';
33
export * from './interface';
44
export * from './decorator';
5-
export { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';

packages/mcp/src/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
NextFunction as BaseNextFunction,
66
} from '@midwayjs/core';
77
import { Implementation, CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
8-
import { ServerOptions} from '@modelcontextprotocol/sdk/server/index.js';
8+
import { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
99

1010
export interface IMidwayMCPConfigurationOptions extends IConfigurationOptions {
1111
serverInfo: Implementation;

packages/web-express/src/framework.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class MidwayExpressFramework extends BaseFramework<
5656
debug('[express]: create express app');
5757
this.app = express() as unknown as IMidwayExpressApplication;
5858
debug('[express]: use root middleware');
59+
5960
// use root middleware
6061
this.app.use((req, res, next) => {
6162
const ctx = req as Context;
@@ -66,6 +67,14 @@ export class MidwayExpressFramework extends BaseFramework<
6667
next();
6768
});
6869

70+
// 版本控制配置
71+
const versioningConfig = this.configurationOptions.versioning;
72+
73+
// 如果启用版本控制,添加版本处理中间件
74+
if (versioningConfig?.enabled) {
75+
this.app.use(this.createVersioningMiddleware(versioningConfig));
76+
}
77+
6978
this.defineApplicationProperties({
7079
useMiddleware: (
7180
routerPath:
@@ -414,4 +423,60 @@ export class MidwayExpressFramework extends BaseFramework<
414423
public getFrameworkName() {
415424
return 'express';
416425
}
426+
427+
private createVersioningMiddleware(config: any) {
428+
return (req: Context, res: Response, next: NextFunction) => {
429+
// 提取版本信息
430+
const version = this.extractVersion(req, config);
431+
req.apiVersion = version;
432+
433+
// 对于 URI 版本控制,重写路径
434+
if (config.type === 'URI' && version) {
435+
const versionPrefix = `/${config.prefix || 'v'}${version}`;
436+
if (req.path.startsWith(versionPrefix)) {
437+
req.originalPath = req.path;
438+
// Express 中需要修改 url 而不是 path
439+
req.url = req.url.replace(versionPrefix, '') || '/';
440+
}
441+
}
442+
443+
next();
444+
};
445+
}
446+
447+
private extractVersion(req: Context, config: any): string | undefined {
448+
// 自定义提取函数优先
449+
if (config.extractVersionFn) {
450+
return config.extractVersionFn(req);
451+
}
452+
453+
const type = config.type || 'URI';
454+
455+
switch (type) {
456+
case 'HEADER': {
457+
const headerName = config.header || 'x-api-version';
458+
const headerValue = req.headers[headerName];
459+
if (typeof headerValue === 'string') {
460+
return headerValue.replace(/^v/, '');
461+
}
462+
return undefined;
463+
}
464+
465+
case 'MEDIA_TYPE': {
466+
const accept = req.headers.accept;
467+
const paramName = config.mediaTypeParam || 'version';
468+
const match = accept?.match(new RegExp(`${paramName}=(\\\\d+)`));
469+
return match ? match[1] : undefined;
470+
}
471+
472+
case 'URI': {
473+
const prefix = config.prefix || 'v';
474+
const uriMatch = req.path.match(new RegExp(`^/${prefix}(\\\\d+)`));
475+
return uriMatch ? uriMatch[1] : undefined;
476+
}
477+
478+
default:
479+
return config.defaultVersion;
480+
}
481+
}
417482
}

packages/web-express/src/interface.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,16 @@ import { ListenOptions } from 'net';
1818
type Request = IMidwayContext<ExpressRequest>;
1919
export type Response = ExpressResponse;
2020
export type NextFunction = ExpressNextFunction;
21-
export interface Context extends Request {}
21+
export interface Context extends Request {
22+
/**
23+
* API version extracted from request
24+
*/
25+
apiVersion?: string;
26+
/**
27+
* Original path before version processing
28+
*/
29+
originalPath?: string;
30+
}
2231

2332
/**
2433
* @deprecated use Context
@@ -90,6 +99,39 @@ export interface IMidwayExpressConfigurationOptions extends IConfigurationOption
9099
* listen options
91100
*/
92101
listenOptions?: ListenOptions;
102+
/**
103+
* API versioning configuration
104+
*/
105+
versioning?: {
106+
/**
107+
* Enable versioning
108+
*/
109+
enabled: boolean;
110+
/**
111+
* Versioning type
112+
*/
113+
type?: 'URI' | 'HEADER' | 'MEDIA_TYPE' | 'CUSTOM';
114+
/**
115+
* Default version if none is specified
116+
*/
117+
defaultVersion?: string;
118+
/**
119+
* Version prefix for URI versioning (default: 'v')
120+
*/
121+
prefix?: string;
122+
/**
123+
* Header name for HEADER versioning (default: 'x-api-version')
124+
*/
125+
header?: string;
126+
/**
127+
* Media type parameter name for MEDIA_TYPE versioning (default: 'version')
128+
*/
129+
mediaTypeParam?: string;
130+
/**
131+
* Custom version extraction function
132+
*/
133+
extractVersionFn?: (ctx: Context) => string | undefined;
134+
};
93135
}
94136

95137
export type Application = IMidwayExpressApplication;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "express-versioning-test",
3+
"version": "1.0.0",
4+
"private": true
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
export const keys = 'test key';
4+
5+
export const express = {
6+
versioning: {
7+
enabled: true,
8+
type: 'URI',
9+
prefix: 'v'
10+
}
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
exports.hello = {
2+
b: 4,
3+
c: 3,
4+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Configuration, Inject } from '@midwayjs/core';
2+
import * as express from '../../../../src';
3+
import { join } from 'path';
4+
5+
@Configuration({
6+
imports: [
7+
express
8+
],
9+
importConfigs: [
10+
join(__dirname, './config'),
11+
]
12+
})
13+
export class AutoConfiguration {
14+
@Inject()
15+
framework: express.Framework;
16+
}

0 commit comments

Comments
 (0)