zentao-api 0.3.0 → 0.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.
@@ -7,7 +7,7 @@ export const MODULES = BUILTIN_MODULES;
7
7
  // - 深克隆:避免用户后续修改自己的输入对象时污染注册表;
8
8
  // - 深冻结:让 getModule / getModuleAction 可以零拷贝返回引用,
9
9
  // 外部尝试改写会在严格模式下抛 TypeError,开销也降到 O(1) 查询。
10
- let modules = freezeModules(deepClone(BUILTIN_MODULES));
10
+ let modules = freezeModules(cloneBuiltinModules());
11
11
  let moduleMap = buildModuleMap(modules);
12
12
  function deepClone(value) {
13
13
  if (Array.isArray(value)) {
@@ -22,6 +22,9 @@ function deepClone(value) {
22
22
  }
23
23
  return value;
24
24
  }
25
+ function cloneBuiltinModules() {
26
+ return deepClone(BUILTIN_MODULES);
27
+ }
25
28
  function deepFreeze(value) {
26
29
  if (value === null || typeof value !== 'object')
27
30
  return value;
@@ -216,6 +219,6 @@ export function isModuleName(moduleName) {
216
219
  }
217
220
  /** @internal */
218
221
  export function resetModuleDefinitions() {
219
- modules = freezeModules(deepClone(BUILTIN_MODULES));
222
+ modules = freezeModules(cloneBuiltinModules());
220
223
  rebuildMap();
221
224
  }
@@ -1,16 +1,116 @@
1
- import type { RequestOptions, ResponseData } from '../types/index.js';
1
+ import type { DataRecord, RequestOptions, ResponseData } from '../types/index.js';
2
+ import type { BUILTIN_MODULES } from '../modules/generated.js';
3
+ type BuiltinModuleDefinition = (typeof BUILTIN_MODULES)[number];
4
+ type BuiltinModuleName = BuiltinModuleDefinition['name'];
5
+ type BuiltinAction<M extends BuiltinModuleName> = Extract<BuiltinModuleDefinition, {
6
+ name: M;
7
+ }>['actions'][number];
8
+ type BuiltinActionName<M extends BuiltinModuleName> = BuiltinAction<M>['name'] & string;
9
+ type BuiltinListRequestName = BuiltinModuleName;
10
+ type BuiltinNamedRequestName = {
11
+ [M in BuiltinModuleName]: `${M}/${BuiltinActionName<M>}`;
12
+ }[BuiltinModuleName];
13
+ type BuiltinIdRequestName = `${BuiltinModuleName}/${number}`;
14
+ /** 内置模块支持的请求名:`module`、`module/action` 或 `module/123`。 */
15
+ export type BuiltinRequestName = BuiltinListRequestName | BuiltinNamedRequestName | BuiltinIdRequestName;
16
+ type ModuleNameOf<Name extends BuiltinRequestName> = Name extends `${infer M}/${string}` ? Extract<M, BuiltinModuleName> : Extract<Name, BuiltinModuleName>;
17
+ type ActionNameOf<Name extends BuiltinRequestName> = Name extends `${string}/${infer A}` ? A extends `${number}` ? 'get' : A : 'list';
18
+ type ActionOfRequest<Name extends BuiltinRequestName> = Extract<BuiltinAction<ModuleNameOf<Name>>, {
19
+ name: ActionNameOf<Name>;
20
+ }>;
21
+ type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer R) => void ? R : never;
22
+ type NumericInput = number | `${number}`;
23
+ type BooleanInput = boolean | 0 | 1 | 'true' | 'false' | '1' | '0' | 'yes' | 'no' | 'on' | 'off';
24
+ type OptionValue<T> = T extends readonly (infer Option)[] ? Option extends {
25
+ value: infer Value;
26
+ } ? Value : never : never;
27
+ type ParamInput<T> = (T extends {
28
+ options: infer Options;
29
+ } ? OptionValue<Options> : never) | (T extends {
30
+ type: 'number' | 'integer';
31
+ } ? NumericInput : T extends {
32
+ type: 'boolean';
33
+ } ? BooleanInput : string);
34
+ type QueryParams<A> = A extends {
35
+ params: readonly (infer Param)[];
36
+ } ? UnionToIntersection<Param extends {
37
+ name: infer Name extends string;
38
+ } ? {
39
+ [K in Name]?: ParamInput<Param>;
40
+ } : unknown> : {};
41
+ type SchemaProperties<S> = S extends {
42
+ properties: infer Properties;
43
+ } ? Properties : {};
44
+ type SchemaRequiredKeys<S> = S extends {
45
+ required: readonly (infer Key)[];
46
+ } ? Extract<Key, keyof SchemaProperties<S> & string> : never;
47
+ type SchemaValue<S> = S extends {
48
+ type: 'integer' | 'number';
49
+ } ? NumericInput : S extends {
50
+ type: 'boolean';
51
+ } ? BooleanInput : S extends {
52
+ type: 'array';
53
+ items?: infer Items;
54
+ } ? Array<SchemaValue<Items>> | readonly SchemaValue<Items>[] | string | Record<string, unknown> : S extends {
55
+ type: 'object';
56
+ } ? Record<string, unknown> : string;
57
+ type BodyParams<A> = A extends {
58
+ requestBody: {
59
+ schema: infer Schema;
60
+ };
61
+ } ? {
62
+ [K in SchemaRequiredKeys<Schema>]: SchemaValue<SchemaProperties<Schema>[K]>;
63
+ } & {
64
+ [K in Exclude<keyof SchemaProperties<Schema> & string, SchemaRequiredKeys<Schema>>]?: SchemaValue<SchemaProperties<Schema>[K]>;
65
+ } & {
66
+ data?: string | Partial<{
67
+ [K in keyof SchemaProperties<Schema> & string]: SchemaValue<SchemaProperties<Schema>[K]>;
68
+ }>;
69
+ } : {
70
+ data?: string | Record<string, unknown>;
71
+ };
72
+ type ScopedParams<PathParams> = 'scope' extends keyof PathParams ? {
73
+ product?: string | number;
74
+ productID?: string | number;
75
+ project?: string | number;
76
+ projectID?: string | number;
77
+ execution?: string | number;
78
+ executionID?: string | number;
79
+ } : {};
80
+ type PathParams<A> = A extends {
81
+ pathParams: infer Params;
82
+ } ? {
83
+ [K in Exclude<keyof Params & string, 'scope' | 'scopeID'>]?: string | number;
84
+ } & ScopedParams<Params> & {
85
+ id?: string | number;
86
+ } : {
87
+ id?: string | number;
88
+ };
89
+ /** 根据内置请求名推导出的参数类型。 */
90
+ export type RequestParamsFor<Name extends BuiltinRequestName> = PathParams<ActionOfRequest<Name>> & QueryParams<ActionOfRequest<Name>> & BodyParams<ActionOfRequest<Name>> & {
91
+ page?: string | number;
92
+ recPerPage?: string | number;
93
+ } & Record<string, unknown>;
94
+ /** 根据内置请求名推导出的 `ResponseData.data` 类型。 */
95
+ export type RequestResultFor<Name extends BuiltinRequestName> = ActionOfRequest<Name> extends {
96
+ resultType: 'list';
97
+ } ? DataRecord[] : ActionOfRequest<Name> extends {
98
+ resultType: 'object';
99
+ } ? DataRecord : unknown;
2
100
  /**
3
- * 按模块动作名请求禅道 API。
101
+ * 按模块名或模块动作名请求禅道 API。
4
102
  *
5
103
  * 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
6
104
  * 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
7
105
  * 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
8
106
  *
9
107
  * @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
10
- * @param name - 模块动作名,例如 `product/list`。
108
+ * @param name - 请求名,例如 `product`、`product/list` 或 `product/1`。
11
109
  * @param params - 请求参数。
12
110
  * @param options - 请求选项。
13
111
  * @returns 归一化后的禅道 API 响应。
14
112
  * @throws {ZentaoError} 传输层错误、参数缺失或 `throwOnFail` 启用时的业务失败。
15
113
  */
16
- export declare function request<T = unknown>(name: `${string}/${string}`, params?: Record<string, unknown>, options?: RequestOptions): Promise<ResponseData<T>>;
114
+ export declare function request<Name extends BuiltinRequestName>(name: Name, params?: RequestParamsFor<Name>, options?: RequestOptions): Promise<ResponseData<RequestResultFor<Name>>>;
115
+ export declare function request<T = unknown>(name: string, params?: Record<string, unknown>, options?: RequestOptions): Promise<ResponseData<T>>;
116
+ export {};
@@ -3,17 +3,21 @@ import { getGlobalOptions } from '../misc/global-options.js';
3
3
  import { getModule } from '../modules/registry.js';
4
4
  import { extractPager, extractResult, resolveModuleCommand } from '../modules/resolve.js';
5
5
  import { isRecord, processData } from '../utils/index.js';
6
- /** 将 `moduleName/methodName` 形式的请求名拆成模块名和动作名。 */
6
+ /** 将 `moduleName`、`moduleName/methodName` `moduleName/<objectID>` 请求名拆成模块名、动作名和对象 ID。 */
7
7
  function splitRequestName(name) {
8
- const [moduleName, actionName] = name.split('/');
9
- // 如果没有指定 actionName
8
+ const parts = name.split('/');
9
+ if (parts.length > 2 || !parts[0]) {
10
+ throw new ZentaoError('E_INVALID_REQUEST_NAME');
11
+ }
12
+ const [moduleName, actionName] = parts;
13
+ // 如果没有指定 actionName,按列表动作处理。
10
14
  if (!actionName?.length) {
11
15
  return {
12
16
  moduleName,
13
17
  actionName: 'list',
14
18
  };
15
19
  }
16
- // 如果 actionName 为数值
20
+ // 如果 actionName 为数值,按详情快捷写法处理。
17
21
  if (Number.isInteger(Number(actionName))) {
18
22
  return {
19
23
  moduleName,
@@ -26,6 +30,26 @@ function splitRequestName(name) {
26
30
  actionName,
27
31
  };
28
32
  }
33
+ function stringifyMessage(value) {
34
+ if (typeof value === 'string')
35
+ return value;
36
+ if (value === undefined)
37
+ return undefined;
38
+ try {
39
+ return JSON.stringify(value);
40
+ }
41
+ catch {
42
+ return String(value);
43
+ }
44
+ }
45
+ function extractApiCode(record) {
46
+ for (const key of ['code', 'errorCode', 'errno']) {
47
+ const value = record[key];
48
+ if (typeof value === 'string' || typeof value === 'number')
49
+ return value;
50
+ }
51
+ return undefined;
52
+ }
29
53
  /** 判断本次调用是否携带了需要本地处理列表的选项。 */
30
54
  function hasListProcessing(options) {
31
55
  return Boolean((options.filter && options.filter.length > 0) ||
@@ -73,10 +97,11 @@ function normalizeResponse(command, raw, options) {
73
97
  const record = raw;
74
98
  const status = record.status === 'fail' ? 'fail' : 'success';
75
99
  const data = applyProcessing(extractResult(command.action, record), options);
100
+ const rawMessage = record.message;
76
101
  const pager = extractPager(command.action, record);
77
- return {
102
+ const response = {
78
103
  status,
79
- message: typeof record.message === 'string' ? record.message : undefined,
104
+ message: stringifyMessage(rawMessage),
80
105
  data: data,
81
106
  pager: pager ? {
82
107
  total: Number(pager.recTotal),
@@ -84,21 +109,16 @@ function normalizeResponse(command, raw, options) {
84
109
  recPerPage: Number(pager.recPerPage),
85
110
  } : undefined,
86
111
  };
112
+ if (rawMessage !== undefined && typeof rawMessage !== 'string') {
113
+ response.rawMessage = rawMessage;
114
+ }
115
+ const apiCode = extractApiCode(record);
116
+ if (apiCode !== undefined)
117
+ response.apiCode = apiCode;
118
+ if (status === 'fail')
119
+ response.raw = record;
120
+ return response;
87
121
  }
88
- /**
89
- * 按模块动作名请求禅道 API。
90
- *
91
- * 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
92
- * 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
93
- * 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
94
- *
95
- * @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
96
- * @param name - 模块动作名,例如 `product/list`。
97
- * @param params - 请求参数。
98
- * @param options - 请求选项。
99
- * @returns 归一化后的禅道 API 响应。
100
- * @throws {ZentaoError} 传输层错误、参数缺失或 `throwOnFail` 启用时的业务失败。
101
- */
102
122
  export async function request(name, params = {}, options = {}) {
103
123
  const globals = getGlobalOptions();
104
124
  const client = options.client ?? globals.client;
@@ -110,8 +130,8 @@ export async function request(name, params = {}, options = {}) {
110
130
  // recPerPage 是最常用的列表参数,允许在全局或本次调用中统一覆盖。
111
131
  const recPerPage = params.recPerPage ?? options.recPerPage ?? globals.recPerPage;
112
132
  const mergedParams = {
113
- ...(id !== undefined ? { id } : {}),
114
133
  ...params,
134
+ ...(id !== undefined ? { id } : {}),
115
135
  ...(recPerPage !== undefined ? { recPerPage } : {}),
116
136
  };
117
137
  const command = resolveModuleCommand(module, actionName, mergedParams);
@@ -29,20 +29,32 @@ export interface GlobalOptions {
29
29
  }
30
30
  /** SDK 支持的 HTTP 方法。 */
31
31
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
32
+ /** 请求体序列化方式。 */
33
+ export type ClientRequestBodyType = 'json' | 'form' | 'raw';
34
+ /** 响应体解析方式。 */
35
+ export type ClientResponseType = 'auto' | 'json' | 'text' | 'arrayBuffer' | 'blob' | 'response';
32
36
  /** `ZentaoClient.request()` 的单次请求选项。 */
33
37
  export interface ClientRequestOptions {
34
38
  /** HTTP 方法,默认 `GET`。 */
35
39
  method?: HttpMethod;
36
- /** JSON 请求体;`GET` 请求会忽略该字段。 */
40
+ /** 请求体;`GET` 请求会忽略该字段。普通对象默认按 JSON 发送,`FormData` / `Blob` / `ArrayBuffer` 等会原样发送。 */
37
41
  body?: unknown;
42
+ /** 请求体序列化方式。默认 `json`;传入 `FormData` 等原生 body 时会自动按 `raw` 处理。 */
43
+ bodyType?: ClientRequestBodyType;
44
+ /** 响应体解析方式。默认 `auto`,会优先尝试 JSON,失败后回落为文本。 */
45
+ responseType?: ClientResponseType;
46
+ /** 额外请求头;会与 SDK 自动注入的 `Token` / `Content-Type` 合并。 */
47
+ headers?: HeadersInit;
38
48
  /** URL 查询参数;`undefined` 值会被跳过。 */
39
49
  query?: Record<string, string | number | boolean | undefined>;
50
+ /** 外部取消信号;会与 SDK 自身的超时控制合并。 */
51
+ signal?: AbortSignal;
40
52
  /** 单次请求超时时间,优先级高于全局和客户端默认值。 */
41
53
  timeout?: number;
42
54
  /** 单次请求 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
43
55
  insecure?: boolean;
44
56
  }
45
- /** 高阶 `request("moduleName/methodName")` 的单次调用选项。 */
57
+ /** 高阶 `request("moduleName")` / `request("moduleName/methodName")` / `request("moduleName/<objectID>")` 的单次调用选项。 */
46
58
  export interface RequestOptions extends ProcessListOptions {
47
59
  /** 本次调用使用的客户端;优先级高于全局客户端。 */
48
60
  client?: ZentaoClient;
@@ -64,6 +76,12 @@ export interface ResponseData<T = unknown> {
64
76
  status: 'success' | 'fail';
65
77
  /** 禅道服务端返回的消息。 */
66
78
  message?: string;
79
+ /** 原始消息字段;当服务端返回对象/数组等非字符串消息时保留在这里。 */
80
+ rawMessage?: unknown;
81
+ /** 服务端返回的业务错误码或状态码字段。 */
82
+ apiCode?: string | number;
83
+ /** 失败响应的原始对象,便于上层展示服务端返回的完整上下文。 */
84
+ raw?: Record<string, unknown>;
67
85
  /** 根据模块动作 `resultGetter` 提取后的业务数据。 */
68
86
  data?: T;
69
87
  /** 统一分页信息。 */
@@ -183,8 +201,8 @@ export type ModuleActionMethod = HttpMethod | Lowercase<HttpMethod>;
183
201
  export type ModuleActionName = ModuleActionType | (string & {});
184
202
  /** 模块动作参数可选项。 */
185
203
  export type ModuleActionParamOption = {
186
- value: unknown;
187
- label: string;
204
+ readonly value: unknown;
205
+ readonly label: string;
188
206
  };
189
207
  /** 模块动作的查询参数定义。 */
190
208
  export interface ModuleActionParam {
@@ -199,7 +217,7 @@ export interface ModuleActionParam {
199
217
  /** 参数值类型,用于基础类型转换。 */
200
218
  type?: 'string' | 'number' | 'boolean';
201
219
  /** 参数可选值。 */
202
- options?: ModuleActionParamOption[];
220
+ options?: readonly ModuleActionParamOption[];
203
221
  }
204
222
  /** 模块动作结果形态。 */
205
223
  export type ModuleActionResultType = 'text' | 'object' | 'list';
@@ -212,7 +230,7 @@ export interface ModuleActionRequestBody {
212
230
  /** 请求体是否必填。 */
213
231
  required?: boolean;
214
232
  /** OpenAPI 风格 schema,用于从 params 组装 body。 */
215
- schema: Record<string, unknown>;
233
+ schema: Readonly<Record<string, unknown>>;
216
234
  /** 请求体示例。 */
217
235
  example?: unknown;
218
236
  }
@@ -221,7 +239,7 @@ export interface ModuleActionResponse {
221
239
  /** 响应说明。 */
222
240
  description?: string;
223
241
  /** 响应 schema。 */
224
- schema: Record<string, unknown>;
242
+ schema: Readonly<Record<string, unknown>>;
225
243
  /** 响应示例。 */
226
244
  example?: unknown;
227
245
  }
@@ -253,9 +271,9 @@ export interface ModuleAction {
253
271
  /** API 路径模板,可包含 `{productID}` 等路径参数。 */
254
272
  path: string;
255
273
  /** 路径参数定义;字符串为说明,对象可携带默认值和可选项。 */
256
- pathParams?: Record<string, string | Omit<ModuleActionParam, 'name'>>;
274
+ pathParams?: Readonly<Record<string, string | Omit<ModuleActionParam, 'name'>>>;
257
275
  /** 查询参数定义。 */
258
- params?: ModuleActionParam[];
276
+ params?: readonly ModuleActionParam[];
259
277
  /** 请求体定义。 */
260
278
  requestBody?: ModuleActionRequestBody;
261
279
  /** 结果形态。 */
@@ -278,7 +296,7 @@ export interface ModuleDefinition {
278
296
  /** 模块说明。 */
279
297
  description?: string;
280
298
  /** 模块支持的动作集合。 */
281
- actions: ModuleAction[];
299
+ actions: readonly ModuleAction[];
282
300
  }
283
301
  /** 本地数据处理的基础记录类型,对应一条对象数据。 */
284
302
  export type DataRecord = Record<string, unknown>;
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
- const fallbackBuild = "2026-06-27T10:57:52.736Z";
2
- const fallbackVersion = "0.3.0";
1
+ const fallbackBuild = "2026-06-27T12:02:58.054Z";
2
+ const fallbackVersion = "0.3.1";
3
3
  /**
4
4
  * 构建标识,由构建脚本通过 `__ZENTAO_API_BUILD__` 注入。
5
5
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zentao-api",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Browser and Node.js SDK for ZenTao API",
6
6
  "license": "MIT",
@@ -49,6 +49,7 @@
49
49
  "registry:check": "bun run scripts/update-registry.ts --check",
50
50
  "build": "rm -rf dist && tsc -p tsconfig.json && bun run scripts/build-browser.ts",
51
51
  "smoke:node": "node scripts/smoke-node.mjs",
52
+ "smoke:browser": "bun run scripts/smoke-browser-bundler.ts",
52
53
  "smoke:package": "bun run scripts/smoke-package.ts",
53
54
  "docs:reference": "typedoc",
54
55
  "docs:zentao-api": "bun run scripts/generate-zentao-api-docs.ts",
@@ -56,7 +57,7 @@
56
57
  "docs:dev": "bun run docs:generate && vitepress dev docs",
57
58
  "docs:build": "bun run docs:generate && vitepress build docs",
58
59
  "docs:preview": "bun run docs:build && vitepress preview docs",
59
- "check": "bun run test:coverage && bun run typecheck && bun run typecheck:tests && bun run registry:check && bun run build && bun run smoke:node && bun run smoke:package",
60
+ "check": "bun run test:coverage && bun run typecheck && bun run typecheck:tests && bun run registry:check && bun run build && bun run smoke:node && bun run smoke:browser && bun run smoke:package",
60
61
  "prepublishOnly": "bun run check"
61
62
  },
62
63
  "devDependencies": {