@faapi/faapi 1.2.1 → 1.3.1
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/cli/index.js +77 -36
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +128 -1
- package/dist/index.js +77 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -87,6 +87,31 @@ interface CookieOptions {
|
|
|
87
87
|
secure?: boolean;
|
|
88
88
|
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* ctx.fail() 的参数类型(对象形式,status 和 code 均可省略)
|
|
92
|
+
*
|
|
93
|
+
* - status: HTTP 状态码(可选,省略时默认 500)
|
|
94
|
+
* - code: 业务错误码(可选,省略时响应 body 里不含 code 字段)
|
|
95
|
+
* - message: 人类可读错误描述(必填)
|
|
96
|
+
*
|
|
97
|
+
* status 和 code 是两个独立维度,无关联:
|
|
98
|
+
* - status 控制 HTTP 状态码
|
|
99
|
+
* - code 是 body 里的业务错误码字段
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* ctx.fail({ message: '出错' }) // HTTP 500, { error: { message: '出错' } }
|
|
103
|
+
* ctx.fail({ status: 404, message: '用户不存在' }) // HTTP 404, { error: { message: '用户不存在' } }
|
|
104
|
+
* ctx.fail({ status: 404, code: 'USER_NOT_FOUND', message: '用户不存在' }) // HTTP 404, { error: { code: 'USER_NOT_FOUND', message: '用户不存在' } }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
interface FailOptions {
|
|
108
|
+
/** HTTP 状态码(可选,省略时默认 500) */
|
|
109
|
+
status?: number;
|
|
110
|
+
/** 业务错误码(可选,省略时响应 body 里不含 code 字段) */
|
|
111
|
+
code?: string;
|
|
112
|
+
/** 人类可读错误描述 */
|
|
113
|
+
message: string;
|
|
114
|
+
}
|
|
90
115
|
/**
|
|
91
116
|
* ctx.config 的类型:用户自定义业务配置
|
|
92
117
|
*
|
|
@@ -192,6 +217,41 @@ interface FaapiContext {
|
|
|
192
217
|
* ```
|
|
193
218
|
*/
|
|
194
219
|
sse(): SseWriter;
|
|
220
|
+
/**
|
|
221
|
+
* 显式包装成功响应(返回 Response 对象)
|
|
222
|
+
*
|
|
223
|
+
* 用 config.response.ok 包裹 data 并返回 Response。
|
|
224
|
+
* 等价于 handler 直接 `return data`(框架自动包裹),但显式调用语义更清晰。
|
|
225
|
+
*
|
|
226
|
+
* 返回 Response 对象,不会被框架自动包裹再次包装(避免双重包裹)。
|
|
227
|
+
*
|
|
228
|
+
* ```ts
|
|
229
|
+
* // 以下两种写法等价(假设配置了 response.ok = (data) => ({ data })):
|
|
230
|
+
* export function GET() {
|
|
231
|
+
* return { id: 1 }; // 自动包裹 → { data: { id: 1 } }
|
|
232
|
+
* }
|
|
233
|
+
* export function GET2(ctx) {
|
|
234
|
+
* return ctx.ok({ id: 1 }); // 显式包裹 → { data: { id: 1 } }
|
|
235
|
+
* }
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
ok(data: unknown): Response;
|
|
239
|
+
/**
|
|
240
|
+
* 返回错误响应(对象形式参数,status 和 code 均可省略)
|
|
241
|
+
*
|
|
242
|
+
* @param options.status HTTP 状态码(可选,省略时默认 500)
|
|
243
|
+
* @param options.code 业务错误码(可选,省略时响应 body 里不含 code 字段)
|
|
244
|
+
* @param options.message 人类可读错误描述(必填)
|
|
245
|
+
*
|
|
246
|
+
* status 和 code 独立无关联:status 控制 HTTP 状态码,code 是 body 里的业务错误码字段。
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* return ctx.fail({ message: '出错' }); // HTTP 500, { error: { message: '出错' } }
|
|
250
|
+
* return ctx.fail({ status: 404, message: '用户不存在' }); // HTTP 404, { error: { message: '用户不存在' } }
|
|
251
|
+
* return ctx.fail({ status: 404, code: 'USER_NOT_FOUND', message: '用户不存在' }); // HTTP 404, { error: { code: 'USER_NOT_FOUND', message: '用户不存在' } }
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
fail(options: FailOptions): Response;
|
|
195
255
|
/**
|
|
196
256
|
* 读取 cookie 值
|
|
197
257
|
*/
|
|
@@ -562,6 +622,50 @@ interface LifecycleContext {
|
|
|
562
622
|
/** 服务器实例 */
|
|
563
623
|
server: node_http.Server;
|
|
564
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* 统一响应包装配置
|
|
627
|
+
*
|
|
628
|
+
* 配置后,框架自动:
|
|
629
|
+
* - 成功响应:handler return 非 Response 的值时,用 ok 函数包裹
|
|
630
|
+
* - 错误响应:ctx.fail() 用 fail 函数包装 body
|
|
631
|
+
*
|
|
632
|
+
* 未配置 response 时,使用框架默认实现:
|
|
633
|
+
* - ok: (data) => ({ data })
|
|
634
|
+
* - fail: ({ status, code, message }) => 省略的字段不放入 error 对象
|
|
635
|
+
*
|
|
636
|
+
* ```ts
|
|
637
|
+
* import type { FaapiConfig } from '@faapi/faapi';
|
|
638
|
+
* export default {
|
|
639
|
+
* response: {
|
|
640
|
+
* // 自定义成功包装(默认 { data })
|
|
641
|
+
* ok: (data) => ({ code: 0, data }),
|
|
642
|
+
* // 自定义错误包装(默认 { error: { message, ...code?, ...status? } })
|
|
643
|
+
* fail: ({ status, code, message }) => ({ error: { code, message } }),
|
|
644
|
+
* },
|
|
645
|
+
* } satisfies FaapiConfig;
|
|
646
|
+
* ```
|
|
647
|
+
*/
|
|
648
|
+
interface ResponseConfig {
|
|
649
|
+
/**
|
|
650
|
+
* 成功响应包装函数
|
|
651
|
+
*
|
|
652
|
+
* handler return 非 Response 的值时调用。
|
|
653
|
+
* 默认: (data) => ({ data })
|
|
654
|
+
*/
|
|
655
|
+
ok?: (data: unknown) => unknown;
|
|
656
|
+
/**
|
|
657
|
+
* 错误响应包装函数
|
|
658
|
+
*
|
|
659
|
+
* ctx.fail() 调用时使用,接收 { status?, code?, message }。
|
|
660
|
+
* status 和 code 均可能为 undefined(用户调用 ctx.fail 时省略则不传),
|
|
661
|
+
* 默认实现只把非 undefined 的字段放入 error 对象。
|
|
662
|
+
*/
|
|
663
|
+
fail?: (error: {
|
|
664
|
+
status?: number;
|
|
665
|
+
code?: string;
|
|
666
|
+
message: string;
|
|
667
|
+
}) => unknown;
|
|
668
|
+
}
|
|
565
669
|
/**
|
|
566
670
|
* faapi 配置文件类型
|
|
567
671
|
*
|
|
@@ -604,6 +708,29 @@ interface FaapiConfig {
|
|
|
604
708
|
logger?: LoggerOptions | boolean;
|
|
605
709
|
/** HTTP/2 配置,false 禁用(默认 http/1.1) */
|
|
606
710
|
http2?: Http2Options | boolean;
|
|
711
|
+
/**
|
|
712
|
+
* 统一响应包装配置
|
|
713
|
+
*
|
|
714
|
+
* 配置后,框架自动包裹 handler 返回值:
|
|
715
|
+
* - 成功响应:handler return 非 Response → 用 ok 函数包裹(默认 `{ data }`)
|
|
716
|
+
* - 错误响应:ctx.fail() 用 fail 函数包装(默认 `{ error: { message, ...code? } }`)
|
|
717
|
+
*
|
|
718
|
+
* 未配置 response 时,使用框架默认实现(见 ResponseConfig)。
|
|
719
|
+
* 配置 response 后,ok/fail 各字段均可选,按需覆盖。
|
|
720
|
+
*
|
|
721
|
+
* ```ts
|
|
722
|
+
* import type { FaapiConfig } from '@faapi/faapi';
|
|
723
|
+
* export default {
|
|
724
|
+
* response: {
|
|
725
|
+
* ok: (data) => ({ code: 0, data }),
|
|
726
|
+
* fail: ({ status, code, message }) => ({ error: { code, message } }),
|
|
727
|
+
* },
|
|
728
|
+
* } satisfies FaapiConfig;
|
|
729
|
+
* ```
|
|
730
|
+
*
|
|
731
|
+
* 详见 `src/config/configTypes.md` 统一响应包装章节。
|
|
732
|
+
*/
|
|
733
|
+
response?: ResponseConfig;
|
|
607
734
|
/**
|
|
608
735
|
* 全局中间件:对所有路由(HTTP + WebSocket 握手)生效
|
|
609
736
|
*
|
|
@@ -1405,4 +1532,4 @@ type ProdApp = AppBase;
|
|
|
1405
1532
|
*/
|
|
1406
1533
|
declare function createProdApp(options?: CreateAppOptions): Promise<ProdApp>;
|
|
1407
1534
|
|
|
1408
|
-
export { type ProdApp as App, type CorsOptions, type CreateAppOptions, type DevApp, type FaapiConfig, type FaapiContext, type FaapiContextConfig, FaapiError, type FaapiMiddleware, type FaapiPlugin, type HandlerTypeInfo, type HelmetOptions, type InjectOptions, type InjectResponse, type Injector, type InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, type LoggerOptions, MessageQueue, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type RouteInfo, type RouteInputSchema, type RouteManifest, RouteNotFoundError, type RouteOutputSchema, type RouteParamSchema, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, type SseEvent, type SseWriter, type TestServer, type TestServerOptions, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, type WsTestClient, type WsTestClientOptions, collectRouteSchemaSources, connectWs, cors, createProdApp as createApp, createContext, createDevApp, createProdApp, createProgram, createTestServer, extractTypeInfo, getInputTypeForMethod, helmet, invalidateProgramCache, invokeHandler, loadConfig, loadEnv, logger, resolveTypeNode, waitForWsOpen };
|
|
1535
|
+
export { type ProdApp as App, type CorsOptions, type CreateAppOptions, type DevApp, type FaapiConfig, type FaapiContext, type FaapiContextConfig, FaapiError, type FaapiMiddleware, type FaapiPlugin, type FailOptions, type HandlerTypeInfo, type HelmetOptions, type InjectOptions, type InjectResponse, type Injector, type InjectorMap, InternalError, type LifecycleContext, type LifecycleHooks, type LoggerOptions, MessageQueue, MethodNotAllowedError, ModuleLoadError, type PluginContext, type PluginDeclaration, type ProdApp, type PropertyType, type RequestHandler, type ResponseConfig, type RouteInfo, type RouteInputSchema, type RouteManifest, RouteNotFoundError, type RouteOutputSchema, type RouteParamSchema, type RouteSchemaSource, type RuntimeType, SchemaExtractionError, type SseEvent, type SseWriter, type TestServer, type TestServerOptions, type TypeConstraint, type UpgradeHandler, ValidationError, type ValidationErrorCode, type ValidationIssue, type WsContext, type WsEventHandlers, type WsHandler, type WsSocket, type WsTestClient, type WsTestClientOptions, collectRouteSchemaSources, connectWs, cors, createProdApp as createApp, createContext, createDevApp, createProdApp, createProgram, createTestServer, extractTypeInfo, getInputTypeForMethod, helmet, invalidateProgramCache, invokeHandler, loadConfig, loadEnv, logger, resolveTypeNode, waitForWsOpen };
|
package/dist/index.js
CHANGED
|
@@ -1873,6 +1873,41 @@ function createContext(request, params, config = {}, ip = "") {
|
|
|
1873
1873
|
ctxWithSse.__sseResponse = writer.response;
|
|
1874
1874
|
ctxWithSse.__sseWriter = writer;
|
|
1875
1875
|
return writer;
|
|
1876
|
+
},
|
|
1877
|
+
/**
|
|
1878
|
+
* 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
|
|
1879
|
+
*
|
|
1880
|
+
* 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
|
|
1881
|
+
* handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
|
|
1882
|
+
*/
|
|
1883
|
+
ok(data) {
|
|
1884
|
+
const responseConfig = config.response;
|
|
1885
|
+
const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
|
|
1886
|
+
const body = okFn(data);
|
|
1887
|
+
return ctx.json(body);
|
|
1888
|
+
},
|
|
1889
|
+
/**
|
|
1890
|
+
* 返回错误响应(对象形式参数,status 和 code 均可省略)
|
|
1891
|
+
*
|
|
1892
|
+
* - status 省略时 HTTP 状态码默认 500
|
|
1893
|
+
* - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
|
|
1894
|
+
* - status 和 code 独立无关联
|
|
1895
|
+
*
|
|
1896
|
+
* body 用 config.response.fail(或默认实现)包装。
|
|
1897
|
+
*/
|
|
1898
|
+
fail(options) {
|
|
1899
|
+
const responseConfig = config.response;
|
|
1900
|
+
const failFn = responseConfig?.fail ?? ((e) => {
|
|
1901
|
+
const error = { message: e.message };
|
|
1902
|
+
if (e.code !== void 0) error.code = e.code;
|
|
1903
|
+
return { error };
|
|
1904
|
+
});
|
|
1905
|
+
const body = failFn({
|
|
1906
|
+
status: options.status,
|
|
1907
|
+
code: options.code,
|
|
1908
|
+
message: options.message
|
|
1909
|
+
});
|
|
1910
|
+
return ctx.json(body, options.status ?? 500);
|
|
1876
1911
|
}
|
|
1877
1912
|
};
|
|
1878
1913
|
const extend = config?.extendContext;
|
|
@@ -2052,6 +2087,12 @@ async function injectParamsAsync(handler, ctx, body, injectors) {
|
|
|
2052
2087
|
}
|
|
2053
2088
|
|
|
2054
2089
|
// src/runtime/invokeHandler.ts
|
|
2090
|
+
function wrapResult(result, ctx) {
|
|
2091
|
+
if (result instanceof Response) return result;
|
|
2092
|
+
const responseConfig = ctx.config.response;
|
|
2093
|
+
const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
|
|
2094
|
+
return okFn(result);
|
|
2095
|
+
}
|
|
2055
2096
|
function mergeMeta(response, meta) {
|
|
2056
2097
|
const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
|
|
2057
2098
|
if (!hasMeta) return response;
|
|
@@ -2116,7 +2157,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
2116
2157
|
const result = await injectParamsAsync(handler, ctx, body, injectors);
|
|
2117
2158
|
const sseResponse = pickSseAndAutoClose();
|
|
2118
2159
|
if (sseResponse) return sseResponse;
|
|
2119
|
-
return toResponse(result, meta);
|
|
2160
|
+
return toResponse(wrapResult(result, ctx), meta);
|
|
2120
2161
|
} catch (err) {
|
|
2121
2162
|
autoCloseSseOnError();
|
|
2122
2163
|
throw err;
|
|
@@ -2127,7 +2168,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
|
|
|
2127
2168
|
const result = await injectParamsAsync(handler, ctx, body, injectors);
|
|
2128
2169
|
const sseResponse = pickSseAndAutoClose();
|
|
2129
2170
|
if (sseResponse) return sseResponse;
|
|
2130
|
-
return toResponse(result, meta);
|
|
2171
|
+
return toResponse(wrapResult(result, ctx), meta);
|
|
2131
2172
|
} catch (err) {
|
|
2132
2173
|
autoCloseSseOnError();
|
|
2133
2174
|
throw err;
|
|
@@ -3900,7 +3941,8 @@ var FAAPI_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
3900
3941
|
"helmet",
|
|
3901
3942
|
"bodyLimit",
|
|
3902
3943
|
"logger",
|
|
3903
|
-
"http2"
|
|
3944
|
+
"http2",
|
|
3945
|
+
"response"
|
|
3904
3946
|
]);
|
|
3905
3947
|
function isFaapiConfigKey(key) {
|
|
3906
3948
|
return FAAPI_CONFIG_KEYS.has(key);
|
|
@@ -4006,41 +4048,40 @@ async function createAppBase(options) {
|
|
|
4006
4048
|
} = injectOpts ?? {};
|
|
4007
4049
|
const queryStr = query ? "?" + new URLSearchParams(Object.entries(query).map(([k, v]) => [k, String(v)])).toString() : "";
|
|
4008
4050
|
return new Promise((resolve, reject) => {
|
|
4009
|
-
const
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
}
|
|
4026
|
-
},
|
|
4027
|
-
end(data) {
|
|
4028
|
-
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data ?? "");
|
|
4029
|
-
this._body = buf;
|
|
4030
|
-
resolve({
|
|
4031
|
-
status: this.statusCode,
|
|
4032
|
-
headers: new Headers(this._headers),
|
|
4033
|
-
body: this.parseBody()
|
|
4034
|
-
});
|
|
4035
|
-
},
|
|
4036
|
-
parseBody() {
|
|
4037
|
-
try {
|
|
4038
|
-
return JSON.parse(this._body.toString());
|
|
4039
|
-
} catch {
|
|
4040
|
-
return this._body.toString();
|
|
4041
|
-
}
|
|
4051
|
+
const chunks = [];
|
|
4052
|
+
const mockRes = new PassThrough();
|
|
4053
|
+
mockRes.statusCode = 200;
|
|
4054
|
+
mockRes._headers = {};
|
|
4055
|
+
mockRes.setHeader = function(name, value) {
|
|
4056
|
+
this._headers[name.toLowerCase()] = value;
|
|
4057
|
+
};
|
|
4058
|
+
mockRes.appendHeader = function(name, value) {
|
|
4059
|
+
const key = name.toLowerCase();
|
|
4060
|
+
const existing = this._headers[key];
|
|
4061
|
+
this._headers[key] = existing ? `${existing}, ${value}` : value;
|
|
4062
|
+
};
|
|
4063
|
+
mockRes.writeHead = function(status, headers) {
|
|
4064
|
+
this.statusCode = status;
|
|
4065
|
+
if (headers) {
|
|
4066
|
+
Object.assign(this._headers, headers);
|
|
4042
4067
|
}
|
|
4043
4068
|
};
|
|
4069
|
+
mockRes.on("data", (chunk) => chunks.push(chunk));
|
|
4070
|
+
mockRes.on("error", reject);
|
|
4071
|
+
mockRes.on("finish", () => {
|
|
4072
|
+
const body2 = Buffer.concat(chunks);
|
|
4073
|
+
let parsed;
|
|
4074
|
+
try {
|
|
4075
|
+
parsed = JSON.parse(body2.toString());
|
|
4076
|
+
} catch {
|
|
4077
|
+
parsed = body2.toString();
|
|
4078
|
+
}
|
|
4079
|
+
resolve({
|
|
4080
|
+
status: mockRes.statusCode,
|
|
4081
|
+
headers: new Headers(mockRes._headers),
|
|
4082
|
+
body: parsed
|
|
4083
|
+
});
|
|
4084
|
+
});
|
|
4044
4085
|
const listeners = server.listeners("request");
|
|
4045
4086
|
const handler = listeners[listeners.length - 1];
|
|
4046
4087
|
if (typeof handler !== "function") {
|