@faapi/faapi 1.2.1 → 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/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 mockRes = {
4010
- statusCode: 200,
4011
- _headers: {},
4012
- _body: Buffer.alloc(0),
4013
- setHeader(name, value) {
4014
- this._headers[name.toLowerCase()] = value;
4015
- },
4016
- appendHeader(name, value) {
4017
- const key = name.toLowerCase();
4018
- const existing = this._headers[key];
4019
- this._headers[key] = existing ? `${existing}, ${value}` : value;
4020
- },
4021
- writeHead(status, headers) {
4022
- this.statusCode = status;
4023
- if (headers) {
4024
- Object.assign(this._headers, headers);
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") {