zentao-api 0.2.1 → 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 {};
@@ -2,17 +2,22 @@ import { ZentaoError } from '../misc/errors.js';
2
2
  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
- /** `moduleName/methodName` 形式的请求名拆成模块名和动作名。 */
5
+ import { isRecord, processData } from '../utils/index.js';
6
+ /** 将 `moduleName`、`moduleName/methodName` 或 `moduleName/<objectID>` 请求名拆成模块名、动作名和对象 ID。 */
6
7
  function splitRequestName(name) {
7
- const [moduleName, actionName] = name.split('/');
8
- // 如果没有指定 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,按列表动作处理。
9
14
  if (!actionName?.length) {
10
15
  return {
11
16
  moduleName,
12
17
  actionName: 'list',
13
18
  };
14
19
  }
15
- // 如果 actionName 为数值
20
+ // 如果 actionName 为数值,按详情快捷写法处理。
16
21
  if (Number.isInteger(Number(actionName))) {
17
22
  return {
18
23
  moduleName,
@@ -25,22 +30,78 @@ function splitRequestName(name) {
25
30
  actionName,
26
31
  };
27
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
+ }
53
+ /** 判断本次调用是否携带了需要本地处理列表的选项。 */
54
+ function hasListProcessing(options) {
55
+ return Boolean((options.filter && options.filter.length > 0) ||
56
+ (options.search && options.search.length > 0) ||
57
+ options.sort ||
58
+ options.limit ||
59
+ (options.pick && options.pick.length > 0));
60
+ }
61
+ /**
62
+ * 对归一化后的业务数据应用本地处理(过滤 → 搜索 → 排序 → 限制数量 → 摘取)。
63
+ *
64
+ * - 对象列表:交由 {@link processData} 完整处理。
65
+ * - 基本类型数组:仅 `limit` 生效(按数量截断),避免破坏原始元素。
66
+ * - 单条对象:只有 `pick` 生效。
67
+ * - 其他形态原样返回。
68
+ */
69
+ function applyProcessing(data, options) {
70
+ if (Array.isArray(data)) {
71
+ if (!hasListProcessing(options))
72
+ return data;
73
+ if (data.every(isRecord)) {
74
+ return processData(data, {
75
+ filter: options.filter,
76
+ search: options.search,
77
+ searchFields: options.searchFields,
78
+ sort: options.sort,
79
+ limit: options.limit,
80
+ pick: options.pick,
81
+ });
82
+ }
83
+ // 非对象数组(如 ID 列表)只能按数量截断,其余处理不适用。
84
+ const limit = Number(options.limit);
85
+ return Number.isFinite(limit) && limit >= 0 ? data.slice(0, Math.floor(limit)) : data;
86
+ }
87
+ if (isRecord(data) && options.pick && options.pick.length > 0) {
88
+ return processData(data, { pick: options.pick });
89
+ }
90
+ return data;
91
+ }
28
92
  /** 将禅道原始响应归一化为稳定的 ResponseData 结构。 */
29
- function normalizeResponse(command, raw, limit) {
93
+ function normalizeResponse(command, raw, options) {
30
94
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
31
95
  return { status: 'success', data: raw };
32
96
  }
33
97
  const record = raw;
34
98
  const status = record.status === 'fail' ? 'fail' : 'success';
35
- let data = extractResult(command.action, record);
36
- const numericLimit = limit === undefined ? undefined : Number(limit);
37
- if (Array.isArray(data) && numericLimit !== undefined && !Number.isNaN(numericLimit)) {
38
- data = data.slice(0, numericLimit);
39
- }
99
+ const data = applyProcessing(extractResult(command.action, record), options);
100
+ const rawMessage = record.message;
40
101
  const pager = extractPager(command.action, record);
41
- return {
102
+ const response = {
42
103
  status,
43
- message: typeof record.message === 'string' ? record.message : undefined,
104
+ message: stringifyMessage(rawMessage),
44
105
  data: data,
45
106
  pager: pager ? {
46
107
  total: Number(pager.recTotal),
@@ -48,21 +109,16 @@ function normalizeResponse(command, raw, limit) {
48
109
  recPerPage: Number(pager.recPerPage),
49
110
  } : undefined,
50
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;
51
121
  }
52
- /**
53
- * 按模块动作名请求禅道 API。
54
- *
55
- * 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
56
- * 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
57
- * 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
58
- *
59
- * @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
60
- * @param name - 模块动作名,例如 `product/list`。
61
- * @param params - 请求参数。
62
- * @param options - 请求选项。
63
- * @returns 归一化后的禅道 API 响应。
64
- * @throws {ZentaoError} 传输层错误、参数缺失或 `throwOnFail` 启用时的业务失败。
65
- */
66
122
  export async function request(name, params = {}, options = {}) {
67
123
  const globals = getGlobalOptions();
68
124
  const client = options.client ?? globals.client;
@@ -74,8 +130,8 @@ export async function request(name, params = {}, options = {}) {
74
130
  // recPerPage 是最常用的列表参数,允许在全局或本次调用中统一覆盖。
75
131
  const recPerPage = params.recPerPage ?? options.recPerPage ?? globals.recPerPage;
76
132
  const mergedParams = {
77
- ...(id !== undefined ? { id } : {}),
78
133
  ...params,
134
+ ...(id !== undefined ? { id } : {}),
79
135
  ...(recPerPage !== undefined ? { recPerPage } : {}),
80
136
  };
81
137
  const command = resolveModuleCommand(module, actionName, mergedParams);
@@ -86,7 +142,9 @@ export async function request(name, params = {}, options = {}) {
86
142
  timeout: options.timeout ?? globals.timeout,
87
143
  insecure: options.insecure ?? globals.insecure,
88
144
  });
89
- const response = normalizeResponse(command, raw, options.limit ?? globals.limit);
145
+ // limit 现归入本地处理选项;本次调用优先,缺省回落到全局默认。
146
+ const processOptions = { ...options, limit: options.limit ?? globals.limit };
147
+ const response = normalizeResponse(command, raw, processOptions);
90
148
  if (response.status === 'fail' && (options.throwOnFail ?? globals.throwOnFail)) {
91
149
  throw new ZentaoError('E_API_FAILED', { message: response.message ?? '' }, response);
92
150
  }
@@ -29,27 +29,37 @@ 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")` 的单次调用选项。 */
46
- export interface RequestOptions {
57
+ /** 高阶 `request("moduleName")` / `request("moduleName/methodName")` / `request("moduleName/<objectID>")` 的单次调用选项。 */
58
+ export interface RequestOptions extends ProcessListOptions {
47
59
  /** 本次调用使用的客户端;优先级高于全局客户端。 */
48
60
  client?: ZentaoClient;
49
61
  /** 本次调用使用的每页记录数,优先级高于全局 `recPerPage`。 */
50
62
  recPerPage?: string;
51
- /** 本次调用限制返回列表数量,优先级高于全局 `limit`。 */
52
- limit?: string;
53
63
  /** 本次调用超时时间。 */
54
64
  timeout?: number;
55
65
  /** 本次调用 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
@@ -66,6 +76,12 @@ export interface ResponseData<T = unknown> {
66
76
  status: 'success' | 'fail';
67
77
  /** 禅道服务端返回的消息。 */
68
78
  message?: string;
79
+ /** 原始消息字段;当服务端返回对象/数组等非字符串消息时保留在这里。 */
80
+ rawMessage?: unknown;
81
+ /** 服务端返回的业务错误码或状态码字段。 */
82
+ apiCode?: string | number;
83
+ /** 失败响应的原始对象,便于上层展示服务端返回的完整上下文。 */
84
+ raw?: Record<string, unknown>;
69
85
  /** 根据模块动作 `resultGetter` 提取后的业务数据。 */
70
86
  data?: T;
71
87
  /** 统一分页信息。 */
@@ -185,8 +201,8 @@ export type ModuleActionMethod = HttpMethod | Lowercase<HttpMethod>;
185
201
  export type ModuleActionName = ModuleActionType | (string & {});
186
202
  /** 模块动作参数可选项。 */
187
203
  export type ModuleActionParamOption = {
188
- value: unknown;
189
- label: string;
204
+ readonly value: unknown;
205
+ readonly label: string;
190
206
  };
191
207
  /** 模块动作的查询参数定义。 */
192
208
  export interface ModuleActionParam {
@@ -201,7 +217,7 @@ export interface ModuleActionParam {
201
217
  /** 参数值类型,用于基础类型转换。 */
202
218
  type?: 'string' | 'number' | 'boolean';
203
219
  /** 参数可选值。 */
204
- options?: ModuleActionParamOption[];
220
+ options?: readonly ModuleActionParamOption[];
205
221
  }
206
222
  /** 模块动作结果形态。 */
207
223
  export type ModuleActionResultType = 'text' | 'object' | 'list';
@@ -214,7 +230,7 @@ export interface ModuleActionRequestBody {
214
230
  /** 请求体是否必填。 */
215
231
  required?: boolean;
216
232
  /** OpenAPI 风格 schema,用于从 params 组装 body。 */
217
- schema: Record<string, unknown>;
233
+ schema: Readonly<Record<string, unknown>>;
218
234
  /** 请求体示例。 */
219
235
  example?: unknown;
220
236
  }
@@ -223,7 +239,7 @@ export interface ModuleActionResponse {
223
239
  /** 响应说明。 */
224
240
  description?: string;
225
241
  /** 响应 schema。 */
226
- schema: Record<string, unknown>;
242
+ schema: Readonly<Record<string, unknown>>;
227
243
  /** 响应示例。 */
228
244
  example?: unknown;
229
245
  }
@@ -255,9 +271,9 @@ export interface ModuleAction {
255
271
  /** API 路径模板,可包含 `{productID}` 等路径参数。 */
256
272
  path: string;
257
273
  /** 路径参数定义;字符串为说明,对象可携带默认值和可选项。 */
258
- pathParams?: Record<string, string | Omit<ModuleActionParam, 'name'>>;
274
+ pathParams?: Readonly<Record<string, string | Omit<ModuleActionParam, 'name'>>>;
259
275
  /** 查询参数定义。 */
260
- params?: ModuleActionParam[];
276
+ params?: readonly ModuleActionParam[];
261
277
  /** 请求体定义。 */
262
278
  requestBody?: ModuleActionRequestBody;
263
279
  /** 结果形态。 */
@@ -280,7 +296,49 @@ export interface ModuleDefinition {
280
296
  /** 模块说明。 */
281
297
  description?: string;
282
298
  /** 模块支持的动作集合。 */
283
- actions: ModuleAction[];
299
+ actions: readonly ModuleAction[];
300
+ }
301
+ /** 本地数据处理的基础记录类型,对应一条对象数据。 */
302
+ export type DataRecord = Record<string, unknown>;
303
+ /** 单条过滤条件,字段名支持 `.` 访问子字段。 */
304
+ export interface DataRecordFilter {
305
+ /** 字段路径,例如 `status` 或 `assignedTo.id`。 */
306
+ key: string;
307
+ /** 比较运算符。 */
308
+ operator: '=' | '!=' | '>' | '<' | '>=' | '<=' | '~' | '!~';
309
+ /** 比较值;数组用于 `=`/`!=`/`~`/`!~` 的“任一/全不”匹配。 */
310
+ value: string | number | boolean | string[];
311
+ }
312
+ /** 一组过滤条件,组内按 `operator` 组合;多组之间按 AND 组合。 */
313
+ export interface DataRecordFilterGroup {
314
+ /** 组内条件的组合方式。 */
315
+ operator: 'AND' | 'OR';
316
+ /** 组内条件列表。 */
317
+ conditions: DataRecordFilter[];
318
+ }
319
+ /** 排序表达式,格式为 `字段:asc|desc`。 */
320
+ export type SortExpr = `${string}:${'asc' | 'desc'}`;
321
+ /** 自定义排序比较函数。 */
322
+ export type SortFn = (a: DataRecord, b: DataRecord) => number;
323
+ /** {@link processData} 处理列表时的选项;执行顺序为 过滤 → 搜索 → 排序 → 限制数量 → 摘取。 */
324
+ export interface ProcessListOptions {
325
+ /** 过滤表达式列表,例如 `["status=active", "pri>=2"]`,多条之间按 AND 组合。 */
326
+ filter?: string[];
327
+ /** 模糊搜索关键词组,组内空格分隔为 OR,多组之间按 AND 组合。 */
328
+ search?: string[];
329
+ /** 限定搜索字段,缺省时搜索全部字段。 */
330
+ searchFields?: string[];
331
+ /** 排序表达式,多个字段以英文逗号分隔,例如 `pri:desc,id:asc`。 */
332
+ sort?: string;
333
+ /** 限制返回列表数量,在排序后、摘取前截断;不改变服务端页大小。 */
334
+ limit?: string;
335
+ /** 摘取字段路径列表。 */
336
+ pick?: string[];
337
+ }
338
+ /** {@link processData} 处理单条对象时的选项。 */
339
+ export interface ProcessSingleOptions {
340
+ /** 摘取字段路径列表。 */
341
+ pick?: string[];
284
342
  }
285
343
  /** 将模块动作和参数解析后的可执行请求描述。 */
286
344
  export interface ResolvedModuleCommand {
@@ -0,0 +1 @@
1
+ export declare function asArray<T>(value: T | T[]): T[];
@@ -0,0 +1,3 @@
1
+ export function asArray(value) {
2
+ return Array.isArray(value) ? value : [value];
3
+ }
@@ -0,0 +1,24 @@
1
+ import type { DataRecord, DataRecordFilterGroup, ProcessListOptions, ProcessSingleOptions, SortExpr, SortFn } from '../types/index.js';
2
+ /** 从单条对象中摘取指定字段,支持通过 `.` 访问子字段,保留嵌套结构。 */
3
+ export declare function pickFieldsSingle(data: DataRecord, fields: string[]): DataRecord;
4
+ /** 对列表中的每条对象摘取指定字段。 */
5
+ export declare function pickFields(data: DataRecord[], fields: string[]): DataRecord[];
6
+ /** 按条件组过滤列表,多组之间按 AND 组合。 */
7
+ export declare function filterData(data: DataRecord[], filterGroups: DataRecordFilterGroup[]): DataRecord[];
8
+ /**
9
+ * 对列表做大小写不敏感的模糊匹配。
10
+ *
11
+ * 每个 `keywordGroups` 元素是一个关键词串,组内以空白分隔为 OR;多组之间按 AND 组合。
12
+ */
13
+ export declare function searchData(data: DataRecord[], keywordGroups: string[], searchFields?: string[]): DataRecord[];
14
+ /**
15
+ * 对列表排序,返回新数组(不修改入参)。
16
+ *
17
+ * `sortFields` 的每个元素可以是 `字段:asc|desc` 表达式或自定义比较函数,按先后顺序生效;
18
+ * 数值字段按数字比较,否则按字符串 `localeCompare`。
19
+ */
20
+ export declare function sortData(data: DataRecord[], sortFields: (SortExpr | SortFn)[]): DataRecord[];
21
+ /** 处理单条对象:仅支持字段摘取。 */
22
+ export declare function processData(data: DataRecord, options: ProcessSingleOptions): DataRecord;
23
+ /** 处理列表:按 过滤 → 搜索 → 排序 → 限制数量 → 摘取 的顺序执行。 */
24
+ export declare function processData(data: DataRecord[], options: ProcessListOptions): DataRecord[];
Binary file
@@ -1,6 +1,4 @@
1
- /** 判断值是否为普通对象(非数组、非 null)。 */
2
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
- /** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
4
- export declare function normalizeSiteUrl(baseUrl: string): string;
5
- export declare function getNestedValue(obj: unknown, path: string): unknown;
6
- export declare function asArray<T>(value: T | T[]): T[];
1
+ export { isRecord, getNestedValue } from './object.js';
2
+ export { asArray } from './array.js';
3
+ export { normalizeSiteUrl } from './url.js';
4
+ export { pickFields, pickFieldsSingle, filterData, searchData, sortData, processData, } from './data.js';
@@ -1,37 +1,4 @@
1
- import { ZentaoError } from '../misc/errors.js';
2
- /** 判断值是否为普通对象(非数组、非 null)。 */
3
- export function isRecord(value) {
4
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
5
- }
6
- /** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
7
- export function normalizeSiteUrl(baseUrl) {
8
- const trimmed = baseUrl.trim().replace(/\/+$/, '');
9
- if (!trimmed)
10
- throw new ZentaoError('E_INVALID_BASE_URL');
11
- const siteUrl = trimmed.replace(/\/api\.php\/v2$/i, '');
12
- let parsed;
13
- try {
14
- parsed = new URL(siteUrl);
15
- }
16
- catch {
17
- throw new ZentaoError('E_INVALID_BASE_URL');
18
- }
19
- if (!['http:', 'https:'].includes(parsed.protocol) || parsed.search || parsed.hash) {
20
- throw new ZentaoError('E_INVALID_BASE_URL');
21
- }
22
- return parsed.toString().replace(/\/+$/, '');
23
- }
24
- export function getNestedValue(obj, path) {
25
- const keys = path.split('.');
26
- let current = obj;
27
- for (const key of keys) {
28
- if (current === null || current === undefined || typeof current !== 'object') {
29
- return undefined;
30
- }
31
- current = current[key];
32
- }
33
- return current;
34
- }
35
- export function asArray(value) {
36
- return Array.isArray(value) ? value : [value];
37
- }
1
+ export { isRecord, getNestedValue } from './object.js';
2
+ export { asArray } from './array.js';
3
+ export { normalizeSiteUrl } from './url.js';
4
+ export { pickFields, pickFieldsSingle, filterData, searchData, sortData, processData, } from './data.js';
@@ -0,0 +1,3 @@
1
+ /** 判断值是否为普通对象(非数组、非 null)。 */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ export declare function getNestedValue(obj: unknown, path: string): unknown;
@@ -0,0 +1,15 @@
1
+ /** 判断值是否为普通对象(非数组、非 null)。 */
2
+ export function isRecord(value) {
3
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ export function getNestedValue(obj, path) {
6
+ const keys = path.split('.');
7
+ let current = obj;
8
+ for (const key of keys) {
9
+ if (current === null || current === undefined || typeof current !== 'object') {
10
+ return undefined;
11
+ }
12
+ current = current[key];
13
+ }
14
+ return current;
15
+ }
@@ -0,0 +1,2 @@
1
+ /** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
2
+ export declare function normalizeSiteUrl(baseUrl: string): string;
@@ -0,0 +1,19 @@
1
+ import { ZentaoError } from '../misc/errors.js';
2
+ /** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
3
+ export function normalizeSiteUrl(baseUrl) {
4
+ const trimmed = baseUrl.trim().replace(/\/+$/, '');
5
+ if (!trimmed)
6
+ throw new ZentaoError('E_INVALID_BASE_URL');
7
+ const siteUrl = trimmed.replace(/\/api\.php\/v2$/i, '');
8
+ let parsed;
9
+ try {
10
+ parsed = new URL(siteUrl);
11
+ }
12
+ catch {
13
+ throw new ZentaoError('E_INVALID_BASE_URL');
14
+ }
15
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.search || parsed.hash) {
16
+ throw new ZentaoError('E_INVALID_BASE_URL');
17
+ }
18
+ return parsed.toString().replace(/\/+$/, '');
19
+ }
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
- const fallbackBuild = "2026-06-23T08:50:58.737Z";
2
- const fallbackVersion = "0.2.1";
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.2.1",
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": {