@dangao/bun-server 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller/decorators.d.ts +30 -1
- package/dist/controller/decorators.d.ts.map +1 -1
- package/dist/controller/index.d.ts +2 -2
- package/dist/controller/index.d.ts.map +1 -1
- package/dist/controller/param-binder.d.ts +12 -0
- package/dist/controller/param-binder.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -0
- package/package.json +1 -1
- package/src/controller/decorators.ts +55 -0
- package/src/controller/index.ts +16 -2
- package/src/controller/param-binder.ts +87 -1
- package/src/index.ts +17 -2
- package/tests/controller/param-map.test.ts +237 -0
|
@@ -8,7 +8,9 @@ export declare enum ParamType {
|
|
|
8
8
|
PARAM = "param",
|
|
9
9
|
HEADER = "header",
|
|
10
10
|
SESSION = "session",
|
|
11
|
-
CONTEXT = "context"
|
|
11
|
+
CONTEXT = "context",
|
|
12
|
+
QUERY_MAP = "query_map",
|
|
13
|
+
HEADER_MAP = "header_map"
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* 参数元数据
|
|
@@ -17,6 +19,7 @@ export interface ParamMetadata {
|
|
|
17
19
|
type: ParamType;
|
|
18
20
|
key?: string;
|
|
19
21
|
index: number;
|
|
22
|
+
options?: unknown;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* 参数装饰器工厂
|
|
@@ -45,6 +48,32 @@ export declare function Param(key: string): (target: any, propertyKey: string |
|
|
|
45
48
|
* @param key - 请求头键
|
|
46
49
|
*/
|
|
47
50
|
export declare function Header(key: string): (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => void;
|
|
51
|
+
/**
|
|
52
|
+
* QueryMap 注解选项
|
|
53
|
+
*/
|
|
54
|
+
export interface QueryMapOptions<T = unknown> {
|
|
55
|
+
transform?: (input: Record<string, string | string[]>) => T | Promise<T>;
|
|
56
|
+
validate?: (dto: T) => void | Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* HeaderMap 注解选项
|
|
60
|
+
*/
|
|
61
|
+
export interface HeaderMapOptions<T = unknown> {
|
|
62
|
+
normalize?: boolean;
|
|
63
|
+
pick?: string[];
|
|
64
|
+
transform?: (input: Record<string, string | string[]>) => T | Promise<T>;
|
|
65
|
+
validate?: (dto: T) => void | Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* QueryMap 参数装饰器
|
|
69
|
+
* 一次性注入完整查询对象
|
|
70
|
+
*/
|
|
71
|
+
export declare function QueryMap<T = Record<string, string | string[]>>(options?: QueryMapOptions<T> | ((input: Record<string, string | string[]>) => T | Promise<T>)): (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => void;
|
|
72
|
+
/**
|
|
73
|
+
* HeaderMap 参数装饰器
|
|
74
|
+
* 一次性注入完整 headers 对象
|
|
75
|
+
*/
|
|
76
|
+
export declare function HeaderMap<T = Record<string, string | string[]>>(options?: HeaderMapOptions<T> | ((input: Record<string, string | string[]>) => T | Promise<T>)): (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) => void;
|
|
48
77
|
/**
|
|
49
78
|
* Context 参数装饰器
|
|
50
79
|
* 用于在控制器方法中注入当前请求的 Context 对象
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../../src/controller/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAO1B;;GAEG;AACH,oBAAY,SAAS;IACnB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;IACf,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,OAAO,YAAY;
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../../src/controller/decorators.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { Body, Query, Param, Header, Context, getParamMetadata, ParamType } from './decorators';
|
|
2
|
-
export type { ParamMetadata } from './decorators';
|
|
1
|
+
export { Body, Query, QueryMap, Param, Header, HeaderMap, Context, getParamMetadata, ParamType, } from './decorators';
|
|
2
|
+
export type { ParamMetadata, QueryMapOptions, HeaderMapOptions, } from './decorators';
|
|
3
3
|
export { ParamBinder } from './param-binder';
|
|
4
4
|
export { Controller, ControllerRegistry } from './controller';
|
|
5
5
|
export type { ControllerMetadata } from './controller';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controller/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/controller/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,MAAM,EACN,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,aAAa,EACb,eAAe,EACf,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -50,5 +50,17 @@ export declare class ParamBinder {
|
|
|
50
50
|
* @returns Header 值
|
|
51
51
|
*/
|
|
52
52
|
private static getHeaderValue;
|
|
53
|
+
/**
|
|
54
|
+
* 获取 QueryMap 值
|
|
55
|
+
* @param options - 装饰器选项
|
|
56
|
+
* @param context - 请求上下文
|
|
57
|
+
*/
|
|
58
|
+
private static getQueryMapValue;
|
|
59
|
+
/**
|
|
60
|
+
* 获取 HeaderMap 值
|
|
61
|
+
* @param options - 装饰器选项
|
|
62
|
+
* @param context - 请求上下文
|
|
63
|
+
*/
|
|
64
|
+
private static getHeaderMapValue;
|
|
53
65
|
}
|
|
54
66
|
//# sourceMappingURL=param-binder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"param-binder.d.ts","sourceRoot":"","sources":["../../src/controller/param-binder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"param-binder.d.ts","sourceRoot":"","sources":["../../src/controller/param-binder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAQ/C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAK5C;;;GAGG;AACH,qBAAa,WAAW;IACtB;;;;;;;OAOG;WACiB,IAAI,CACtB,MAAM,EAAE,GAAG,EACX,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAChB,SAAS,CAAC,EAAE,SAAS,GACpB,OAAO,CAAC,OAAO,EAAE,CAAC;IAyBrB;;;;;;OAMG;mBACkB,QAAQ;IA4D7B;;;;;OAKG;mBACkB,YAAY;IAWjC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAI5B;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAI5B;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAI7B;;;;OAIG;mBACkB,gBAAgB;IA0BrC;;;;OAIG;mBACkB,iBAAiB;CAuCvC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ export { Route, Router, RouteRegistry } from './router';
|
|
|
6
6
|
export { GET, POST, PUT, DELETE, PATCH } from './router/decorators';
|
|
7
7
|
export type { HttpMethod, RouteHandler, RouteMatch } from './router/types';
|
|
8
8
|
export { BodyParser, RequestWrapper, ResponseBuilder } from './request';
|
|
9
|
-
export { Body, Query, Param, Header, ParamBinder, Controller, ControllerRegistry } from './controller';
|
|
9
|
+
export { Body, Query, QueryMap, Param, Header, HeaderMap, ParamBinder, Controller, ControllerRegistry, } from './controller';
|
|
10
10
|
export { Context as ContextParam } from './controller';
|
|
11
|
-
export type { ParamMetadata, ControllerMetadata } from './controller';
|
|
11
|
+
export type { ParamMetadata, QueryMapOptions, HeaderMapOptions, ControllerMetadata, } from './controller';
|
|
12
12
|
export { Container } from './di/container';
|
|
13
13
|
export { Injectable, Inject } from './di/decorators';
|
|
14
14
|
export { Lifecycle, type ProviderConfig, type DependencyMetadata } from './di/types';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC7F,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1478,6 +1478,22 @@ function Param(key) {
|
|
|
1478
1478
|
function Header(key) {
|
|
1479
1479
|
return createParamDecorator("header" /* HEADER */, key);
|
|
1480
1480
|
}
|
|
1481
|
+
function QueryMap(options) {
|
|
1482
|
+
return function(target, propertyKey, parameterIndex) {
|
|
1483
|
+
const existingParams = Reflect.getMetadata(PARAM_METADATA_KEY, target, propertyKey) || [];
|
|
1484
|
+
const normalizedOptions = typeof options === "function" ? { transform: options } : options ?? {};
|
|
1485
|
+
existingParams.push({ type: "query_map" /* QUERY_MAP */, index: parameterIndex, options: normalizedOptions });
|
|
1486
|
+
Reflect.defineMetadata(PARAM_METADATA_KEY, existingParams, target, propertyKey);
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function HeaderMap(options) {
|
|
1490
|
+
return function(target, propertyKey, parameterIndex) {
|
|
1491
|
+
const existingParams = Reflect.getMetadata(PARAM_METADATA_KEY, target, propertyKey) || [];
|
|
1492
|
+
const normalizedOptions = typeof options === "function" ? { transform: options } : options ?? {};
|
|
1493
|
+
existingParams.push({ type: "header_map" /* HEADER_MAP */, index: parameterIndex, options: normalizedOptions });
|
|
1494
|
+
Reflect.defineMetadata(PARAM_METADATA_KEY, existingParams, target, propertyKey);
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1481
1497
|
function Context2() {
|
|
1482
1498
|
return createParamDecorator("context" /* CONTEXT */);
|
|
1483
1499
|
}
|
|
@@ -1690,6 +1706,10 @@ class ParamBinder {
|
|
|
1690
1706
|
return;
|
|
1691
1707
|
case "context" /* CONTEXT */:
|
|
1692
1708
|
return contextStore.getStore() ?? context;
|
|
1709
|
+
case "query_map" /* QUERY_MAP */:
|
|
1710
|
+
return await this.getQueryMapValue(meta.options, context);
|
|
1711
|
+
case "header_map" /* HEADER_MAP */:
|
|
1712
|
+
return await this.getHeaderMapValue(meta.options, context);
|
|
1693
1713
|
default:
|
|
1694
1714
|
return;
|
|
1695
1715
|
}
|
|
@@ -1713,6 +1733,52 @@ class ParamBinder {
|
|
|
1713
1733
|
static getHeaderValue(key, context) {
|
|
1714
1734
|
return context.getHeader(key);
|
|
1715
1735
|
}
|
|
1736
|
+
static async getQueryMapValue(options, context) {
|
|
1737
|
+
const result = {};
|
|
1738
|
+
const searchParams = context.query;
|
|
1739
|
+
for (const key of searchParams.keys()) {
|
|
1740
|
+
const values = searchParams.getAll(key);
|
|
1741
|
+
if (values.length === 1) {
|
|
1742
|
+
result[key] = values[0];
|
|
1743
|
+
} else {
|
|
1744
|
+
result[key] = values;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
let output = result;
|
|
1748
|
+
if (options?.transform) {
|
|
1749
|
+
output = await options.transform(result);
|
|
1750
|
+
}
|
|
1751
|
+
if (options?.validate) {
|
|
1752
|
+
await options.validate(output);
|
|
1753
|
+
}
|
|
1754
|
+
return output;
|
|
1755
|
+
}
|
|
1756
|
+
static async getHeaderMapValue(options, context) {
|
|
1757
|
+
const normalize = options?.normalize ?? true;
|
|
1758
|
+
const pick = options?.pick?.map((key) => key.toLowerCase());
|
|
1759
|
+
const headers = context.headers;
|
|
1760
|
+
const result = {};
|
|
1761
|
+
headers.forEach((value, rawKey) => {
|
|
1762
|
+
const key = normalize ? rawKey.toLowerCase() : rawKey;
|
|
1763
|
+
if (pick && !pick.includes(key)) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const parts = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1767
|
+
if (parts.length <= 1) {
|
|
1768
|
+
result[key] = parts[0] ?? "";
|
|
1769
|
+
} else {
|
|
1770
|
+
result[key] = parts;
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
let output = result;
|
|
1774
|
+
if (options?.transform) {
|
|
1775
|
+
output = await options.transform(result);
|
|
1776
|
+
}
|
|
1777
|
+
if (options?.validate) {
|
|
1778
|
+
await options.validate(output);
|
|
1779
|
+
}
|
|
1780
|
+
return output;
|
|
1781
|
+
}
|
|
1716
1782
|
}
|
|
1717
1783
|
|
|
1718
1784
|
// src/controller/metadata.ts
|
|
@@ -6782,6 +6848,7 @@ export {
|
|
|
6782
6848
|
QueueService,
|
|
6783
6849
|
QueueModule,
|
|
6784
6850
|
Queue,
|
|
6851
|
+
QueryMap,
|
|
6785
6852
|
Query,
|
|
6786
6853
|
QUEUE_SERVICE_TOKEN,
|
|
6787
6854
|
QUEUE_OPTIONS_TOKEN,
|
|
@@ -6842,6 +6909,7 @@ export {
|
|
|
6842
6909
|
INTERCEPTOR_REGISTRY_TOKEN,
|
|
6843
6910
|
HttpException,
|
|
6844
6911
|
HealthModule,
|
|
6912
|
+
HeaderMap,
|
|
6845
6913
|
Header,
|
|
6846
6914
|
HEALTH_OPTIONS_TOKEN,
|
|
6847
6915
|
HEALTH_INDICATORS_TOKEN,
|
package/package.json
CHANGED
|
@@ -15,6 +15,8 @@ export enum ParamType {
|
|
|
15
15
|
HEADER = 'header',
|
|
16
16
|
SESSION = 'session',
|
|
17
17
|
CONTEXT = 'context',
|
|
18
|
+
QUERY_MAP = 'query_map',
|
|
19
|
+
HEADER_MAP = 'header_map',
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -24,6 +26,7 @@ export interface ParamMetadata {
|
|
|
24
26
|
type: ParamType;
|
|
25
27
|
key?: string;
|
|
26
28
|
index: number;
|
|
29
|
+
options?: unknown;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -73,6 +76,58 @@ export function Header(key: string) {
|
|
|
73
76
|
return createParamDecorator(ParamType.HEADER, key);
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* QueryMap 注解选项
|
|
81
|
+
*/
|
|
82
|
+
export interface QueryMapOptions<T = unknown> {
|
|
83
|
+
transform?: (input: Record<string, string | string[]>) => T | Promise<T>;
|
|
84
|
+
validate?: (dto: T) => void | Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* HeaderMap 注解选项
|
|
89
|
+
*/
|
|
90
|
+
export interface HeaderMapOptions<T = unknown> {
|
|
91
|
+
normalize?: boolean;
|
|
92
|
+
pick?: string[];
|
|
93
|
+
transform?: (input: Record<string, string | string[]>) => T | Promise<T>;
|
|
94
|
+
validate?: (dto: T) => void | Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* QueryMap 参数装饰器
|
|
99
|
+
* 一次性注入完整查询对象
|
|
100
|
+
*/
|
|
101
|
+
export function QueryMap<T = Record<string, string | string[]>>(
|
|
102
|
+
options?: QueryMapOptions<T> | ((input: Record<string, string | string[]>) => T | Promise<T>),
|
|
103
|
+
) {
|
|
104
|
+
return function (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
|
|
105
|
+
const existingParams: ParamMetadata[] =
|
|
106
|
+
Reflect.getMetadata(PARAM_METADATA_KEY, target, propertyKey as string) || [];
|
|
107
|
+
const normalizedOptions: QueryMapOptions<T> =
|
|
108
|
+
typeof options === 'function' ? { transform: options } : options ?? {};
|
|
109
|
+
existingParams.push({ type: ParamType.QUERY_MAP, index: parameterIndex, options: normalizedOptions });
|
|
110
|
+
Reflect.defineMetadata(PARAM_METADATA_KEY, existingParams, target, propertyKey as string);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* HeaderMap 参数装饰器
|
|
116
|
+
* 一次性注入完整 headers 对象
|
|
117
|
+
*/
|
|
118
|
+
export function HeaderMap<T = Record<string, string | string[]>>(
|
|
119
|
+
options?: HeaderMapOptions<T> | ((input: Record<string, string | string[]>) => T | Promise<T>),
|
|
120
|
+
) {
|
|
121
|
+
return function (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
|
|
122
|
+
const existingParams: ParamMetadata[] =
|
|
123
|
+
Reflect.getMetadata(PARAM_METADATA_KEY, target, propertyKey as string) || [];
|
|
124
|
+
const normalizedOptions: HeaderMapOptions<T> =
|
|
125
|
+
typeof options === 'function' ? { transform: options } : options ?? {};
|
|
126
|
+
existingParams.push({ type: ParamType.HEADER_MAP, index: parameterIndex, options: normalizedOptions });
|
|
127
|
+
Reflect.defineMetadata(PARAM_METADATA_KEY, existingParams, target, propertyKey as string);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
76
131
|
/**
|
|
77
132
|
* Context 参数装饰器
|
|
78
133
|
* 用于在控制器方法中注入当前请求的 Context 对象
|
package/src/controller/index.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
Body,
|
|
3
|
+
Query,
|
|
4
|
+
QueryMap,
|
|
5
|
+
Param,
|
|
6
|
+
Header,
|
|
7
|
+
HeaderMap,
|
|
8
|
+
Context,
|
|
9
|
+
getParamMetadata,
|
|
10
|
+
ParamType,
|
|
11
|
+
} from './decorators';
|
|
12
|
+
export type {
|
|
13
|
+
ParamMetadata,
|
|
14
|
+
QueryMapOptions,
|
|
15
|
+
HeaderMapOptions,
|
|
16
|
+
} from './decorators';
|
|
3
17
|
export { ParamBinder } from './param-binder';
|
|
4
18
|
export { Controller, ControllerRegistry } from './controller';
|
|
5
19
|
export type { ControllerMetadata } from './controller';
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Context } from '../core/context';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getParamMetadata,
|
|
4
|
+
ParamType,
|
|
5
|
+
type ParamMetadata,
|
|
6
|
+
type QueryMapOptions,
|
|
7
|
+
type HeaderMapOptions,
|
|
8
|
+
} from './decorators';
|
|
3
9
|
import { Container } from '../di/container';
|
|
4
10
|
import { SessionService } from '../session/service';
|
|
5
11
|
import { SESSION_SERVICE_TOKEN } from '../session/types';
|
|
@@ -106,6 +112,10 @@ export class ParamBinder {
|
|
|
106
112
|
case ParamType.CONTEXT:
|
|
107
113
|
// 从 AsyncLocalStorage 获取当前请求的 Context
|
|
108
114
|
return contextStore.getStore() ?? context;
|
|
115
|
+
case ParamType.QUERY_MAP:
|
|
116
|
+
return await this.getQueryMapValue(meta.options as QueryMapOptions, context);
|
|
117
|
+
case ParamType.HEADER_MAP:
|
|
118
|
+
return await this.getHeaderMapValue(meta.options as HeaderMapOptions, context);
|
|
109
119
|
default:
|
|
110
120
|
return undefined;
|
|
111
121
|
}
|
|
@@ -157,5 +167,81 @@ export class ParamBinder {
|
|
|
157
167
|
private static getHeaderValue(key: string, context: Context): string | null {
|
|
158
168
|
return context.getHeader(key);
|
|
159
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 获取 QueryMap 值
|
|
173
|
+
* @param options - 装饰器选项
|
|
174
|
+
* @param context - 请求上下文
|
|
175
|
+
*/
|
|
176
|
+
private static async getQueryMapValue(
|
|
177
|
+
options: QueryMapOptions | undefined,
|
|
178
|
+
context: Context,
|
|
179
|
+
): Promise<unknown> {
|
|
180
|
+
const result: Record<string, string | string[]> = {};
|
|
181
|
+
const searchParams = context.query;
|
|
182
|
+
// 收集所有键,处理重复 key -> string[]
|
|
183
|
+
for (const key of searchParams.keys()) {
|
|
184
|
+
const values = searchParams.getAll(key);
|
|
185
|
+
if (values.length === 1) {
|
|
186
|
+
result[key] = values[0];
|
|
187
|
+
} else {
|
|
188
|
+
result[key] = values;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let output: unknown = result;
|
|
193
|
+
if (options?.transform) {
|
|
194
|
+
output = await options.transform(result);
|
|
195
|
+
}
|
|
196
|
+
if (options?.validate) {
|
|
197
|
+
await options.validate(output as never);
|
|
198
|
+
}
|
|
199
|
+
return output;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 获取 HeaderMap 值
|
|
204
|
+
* @param options - 装饰器选项
|
|
205
|
+
* @param context - 请求上下文
|
|
206
|
+
*/
|
|
207
|
+
private static async getHeaderMapValue(
|
|
208
|
+
options: HeaderMapOptions | undefined,
|
|
209
|
+
context: Context,
|
|
210
|
+
): Promise<unknown> {
|
|
211
|
+
const normalize = options?.normalize ?? true;
|
|
212
|
+
// Headers API 总是将 header 名称规范化为小写,所以 pick 数组也应该总是小写化
|
|
213
|
+
const pick = options?.pick?.map((key) => key.toLowerCase());
|
|
214
|
+
const headers = context.headers;
|
|
215
|
+
const result: Record<string, string | string[]> = {};
|
|
216
|
+
|
|
217
|
+
headers.forEach((value, rawKey) => {
|
|
218
|
+
// Headers API 总是返回小写的 key,所以 rawKey 已经是小写
|
|
219
|
+
// normalize 选项决定结果中的 key 格式(虽然实际上总是小写)
|
|
220
|
+
const key = normalize ? rawKey.toLowerCase() : rawKey;
|
|
221
|
+
if (pick && !pick.includes(key)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// 处理可能的多值(逗号分隔),统一 trim
|
|
225
|
+
const parts = value
|
|
226
|
+
.split(',')
|
|
227
|
+
.map((item) => item.trim())
|
|
228
|
+
.filter((item) => item.length > 0);
|
|
229
|
+
if (parts.length <= 1) {
|
|
230
|
+
// 单值也保持 trim 后的形态
|
|
231
|
+
result[key] = parts[0] ?? '';
|
|
232
|
+
} else {
|
|
233
|
+
result[key] = parts;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
let output: unknown = result;
|
|
238
|
+
if (options?.transform) {
|
|
239
|
+
output = await options.transform(result);
|
|
240
|
+
}
|
|
241
|
+
if (options?.validate) {
|
|
242
|
+
await options.validate(output as never);
|
|
243
|
+
}
|
|
244
|
+
return output;
|
|
245
|
+
}
|
|
160
246
|
}
|
|
161
247
|
|
package/src/index.ts
CHANGED
|
@@ -6,9 +6,24 @@ export { Route, Router, RouteRegistry } from './router';
|
|
|
6
6
|
export { GET, POST, PUT, DELETE, PATCH } from './router/decorators';
|
|
7
7
|
export type { HttpMethod, RouteHandler, RouteMatch } from './router/types';
|
|
8
8
|
export { BodyParser, RequestWrapper, ResponseBuilder } from './request';
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
Body,
|
|
11
|
+
Query,
|
|
12
|
+
QueryMap,
|
|
13
|
+
Param,
|
|
14
|
+
Header,
|
|
15
|
+
HeaderMap,
|
|
16
|
+
ParamBinder,
|
|
17
|
+
Controller,
|
|
18
|
+
ControllerRegistry,
|
|
19
|
+
} from './controller';
|
|
10
20
|
export { Context as ContextParam } from './controller';
|
|
11
|
-
export type {
|
|
21
|
+
export type {
|
|
22
|
+
ParamMetadata,
|
|
23
|
+
QueryMapOptions,
|
|
24
|
+
HeaderMapOptions,
|
|
25
|
+
ControllerMetadata,
|
|
26
|
+
} from './controller';
|
|
12
27
|
export { Container } from './di/container';
|
|
13
28
|
export { Injectable, Inject } from './di/decorators';
|
|
14
29
|
export { Lifecycle, type ProviderConfig, type DependencyMetadata } from './di/types';
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { ParamBinder } from '../../src/controller/param-binder';
|
|
4
|
+
import {
|
|
5
|
+
QueryMap,
|
|
6
|
+
HeaderMap,
|
|
7
|
+
type QueryMapOptions,
|
|
8
|
+
type HeaderMapOptions,
|
|
9
|
+
} from '../../src/controller/decorators';
|
|
10
|
+
import { Context } from '../../src/core/context';
|
|
11
|
+
|
|
12
|
+
class QueryMapController {
|
|
13
|
+
public constructor(public readonly store: unknown[] = []) {}
|
|
14
|
+
|
|
15
|
+
public async handle(@QueryMap() query: Record<string, string | string[]>) {
|
|
16
|
+
this.store.push(query);
|
|
17
|
+
return query;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async handleTransformed(
|
|
21
|
+
@QueryMap<{ name: string; age: number }>((input) => ({
|
|
22
|
+
name: (input.name as string) ?? '',
|
|
23
|
+
age: Number((input.age as string) ?? '0'),
|
|
24
|
+
}))
|
|
25
|
+
query: { name: string; age: number },
|
|
26
|
+
) {
|
|
27
|
+
this.store.push(query);
|
|
28
|
+
return query;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async handleValidated(
|
|
32
|
+
@QueryMap<{ foo: string }>(
|
|
33
|
+
((input) => ({ foo: input.foo as string })) as QueryMapOptions<{ foo: string }>['transform'],
|
|
34
|
+
)
|
|
35
|
+
query: { foo: string },
|
|
36
|
+
) {
|
|
37
|
+
if (!query.foo) {
|
|
38
|
+
throw new Error('validation failed');
|
|
39
|
+
}
|
|
40
|
+
this.store.push(query);
|
|
41
|
+
return query;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class HeaderMapController {
|
|
46
|
+
public constructor(public readonly store: unknown[] = []) {}
|
|
47
|
+
|
|
48
|
+
public async handle(
|
|
49
|
+
@HeaderMap() headers: Record<string, string | string[]>,
|
|
50
|
+
) {
|
|
51
|
+
this.store.push(headers);
|
|
52
|
+
return headers;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async handlePicked(
|
|
56
|
+
@HeaderMap({
|
|
57
|
+
normalize: true,
|
|
58
|
+
pick: ['x-custom', 'x-list'],
|
|
59
|
+
})
|
|
60
|
+
headers: Record<string, string | string[]>,
|
|
61
|
+
) {
|
|
62
|
+
this.store.push(headers);
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async handlePickedWithNormalizeFalse(
|
|
67
|
+
@HeaderMap({
|
|
68
|
+
normalize: false,
|
|
69
|
+
pick: ['X-Custom', 'X-List'], // 混合大小写
|
|
70
|
+
})
|
|
71
|
+
headers: Record<string, string | string[]>,
|
|
72
|
+
) {
|
|
73
|
+
this.store.push(headers);
|
|
74
|
+
return headers;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async handleTransformed(
|
|
78
|
+
@HeaderMap<{ token: string }>((input) => ({
|
|
79
|
+
token: (input.authorization as string) ?? '',
|
|
80
|
+
}))
|
|
81
|
+
headers: { token: string },
|
|
82
|
+
) {
|
|
83
|
+
this.store.push(headers);
|
|
84
|
+
return headers;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createContext(url: string, init?: RequestInit) {
|
|
89
|
+
return new Context(new Request(url, init));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('ParamBinder QueryMap', () => {
|
|
93
|
+
test('should aggregate query params into map with arrays', async () => {
|
|
94
|
+
const controller = new QueryMapController();
|
|
95
|
+
const ctx = createContext('http://localhost/api?q=1&q=2&name=alice');
|
|
96
|
+
const params = await ParamBinder.bind(
|
|
97
|
+
controller,
|
|
98
|
+
'handle',
|
|
99
|
+
ctx,
|
|
100
|
+
);
|
|
101
|
+
expect(params[0]).toEqual({ q: ['1', '2'], name: 'alice' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should transform query map', async () => {
|
|
105
|
+
const controller = new QueryMapController();
|
|
106
|
+
const ctx = createContext('http://localhost/api?name=alice&age=20');
|
|
107
|
+
const params = await ParamBinder.bind(
|
|
108
|
+
controller,
|
|
109
|
+
'handleTransformed',
|
|
110
|
+
ctx,
|
|
111
|
+
);
|
|
112
|
+
expect(params[0]).toEqual({ name: 'alice', age: 20 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should validate query map via user code', async () => {
|
|
116
|
+
const controller = new QueryMapController();
|
|
117
|
+
const ctx = createContext('http://localhost/api?foo=bar');
|
|
118
|
+
const params = await ParamBinder.bind(
|
|
119
|
+
controller,
|
|
120
|
+
'handleValidated',
|
|
121
|
+
ctx,
|
|
122
|
+
);
|
|
123
|
+
expect(params[0]).toEqual({ foo: 'bar' });
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('ParamBinder HeaderMap', () => {
|
|
128
|
+
test('should aggregate headers into map with optional array', async () => {
|
|
129
|
+
const controller = new HeaderMapController();
|
|
130
|
+
const ctx = createContext('http://localhost/api', {
|
|
131
|
+
headers: {
|
|
132
|
+
'X-Token': 'abc',
|
|
133
|
+
'X-List': 'a, b',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const params = await ParamBinder.bind(
|
|
137
|
+
controller,
|
|
138
|
+
'handle',
|
|
139
|
+
ctx,
|
|
140
|
+
);
|
|
141
|
+
expect(params[0]).toMatchObject({
|
|
142
|
+
'x-token': 'abc',
|
|
143
|
+
'x-list': ['a', 'b'],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('should pick and normalize headers', async () => {
|
|
148
|
+
const controller = new HeaderMapController();
|
|
149
|
+
const ctx = createContext('http://localhost/api', {
|
|
150
|
+
headers: {
|
|
151
|
+
'X-Custom': 'val',
|
|
152
|
+
'X-List': 'a, b',
|
|
153
|
+
'Other': 'ignore',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const params = await ParamBinder.bind(
|
|
157
|
+
controller,
|
|
158
|
+
'handlePicked',
|
|
159
|
+
ctx,
|
|
160
|
+
);
|
|
161
|
+
expect(params[0]).toEqual({
|
|
162
|
+
'x-custom': 'val',
|
|
163
|
+
'x-list': ['a', 'b'],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should pick headers with case-insensitive pick when normalize=true (default)', async () => {
|
|
168
|
+
const controller = new HeaderMapController();
|
|
169
|
+
const ctx = createContext('http://localhost/api', {
|
|
170
|
+
headers: {
|
|
171
|
+
'X-CUSTOM': 'val',
|
|
172
|
+
'X-LIST': 'a, b',
|
|
173
|
+
'Other': 'ignore',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const params = await ParamBinder.bind(
|
|
177
|
+
controller,
|
|
178
|
+
'handlePicked',
|
|
179
|
+
ctx,
|
|
180
|
+
);
|
|
181
|
+
expect(params[0]).toEqual({
|
|
182
|
+
'x-custom': 'val',
|
|
183
|
+
'x-list': ['a', 'b'],
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should transform headers', async () => {
|
|
188
|
+
const controller = new HeaderMapController();
|
|
189
|
+
const ctx = createContext('http://localhost/api', {
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: ' Bearer token123 ',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const params = await ParamBinder.bind(
|
|
195
|
+
controller,
|
|
196
|
+
'handleTransformed',
|
|
197
|
+
ctx,
|
|
198
|
+
);
|
|
199
|
+
expect(params[0]).toEqual({ token: 'Bearer token123' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('should trim single header value when no comma', async () => {
|
|
203
|
+
const controller = new HeaderMapController();
|
|
204
|
+
const ctx = createContext('http://localhost/api', {
|
|
205
|
+
headers: {
|
|
206
|
+
'X-Single': ' abc ',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const params = await ParamBinder.bind(controller, 'handle', ctx);
|
|
210
|
+
expect(params[0]).toMatchObject({
|
|
211
|
+
'x-single': 'abc',
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('should pick headers with mixed-case pick keys when normalize=false', async () => {
|
|
216
|
+
const controller = new HeaderMapController();
|
|
217
|
+
const ctx = createContext('http://localhost/api', {
|
|
218
|
+
headers: {
|
|
219
|
+
'X-Custom': 'val',
|
|
220
|
+
'X-List': 'a, b',
|
|
221
|
+
'Other': 'ignore',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const params = await ParamBinder.bind(
|
|
225
|
+
controller,
|
|
226
|
+
'handlePickedWithNormalizeFalse',
|
|
227
|
+
ctx,
|
|
228
|
+
);
|
|
229
|
+
// Headers API 总是返回小写的 key,所以即使 normalize=false,结果中的 key 也是小写
|
|
230
|
+
expect(params[0]).toEqual({
|
|
231
|
+
'x-custom': 'val',
|
|
232
|
+
'x-list': ['a', 'b'],
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
|