zentao-api 0.3.2 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  import { ZentaoError } from '../misc/errors.js';
2
- import { getNestedValue, isRecord } from '../utils/index.js';
2
+ import { getNestedValue, isBlank, isRecord } from '../utils/index.js';
3
3
  import { getModuleAction } from './registry.js';
4
4
  const SCOPE_MAP = {
5
5
  product: 'products',
@@ -11,7 +11,7 @@ const SCOPE_KEY_ORDER = ['execution', 'project', 'product'];
11
11
  function pickScope(params) {
12
12
  for (const key of SCOPE_KEY_ORDER) {
13
13
  const value = params[key] ?? params[`${key}ID`];
14
- if (value === undefined || value === null || value === '')
14
+ if (isBlank(value))
15
15
  continue;
16
16
  const numberValue = Number(value);
17
17
  if (!Number.isNaN(numberValue)) {
@@ -24,7 +24,7 @@ function pickScope(params) {
24
24
  function resolvePath(action, values) {
25
25
  return action.path.replace(/\{(\w+)\}/g, (_, key) => {
26
26
  const value = values[key];
27
- if (value === undefined || value === '') {
27
+ if (isBlank(value)) {
28
28
  throw new ZentaoError('E_MISSING_PARAM', { param: key });
29
29
  }
30
30
  return String(value);
@@ -37,18 +37,21 @@ function parseData(value) {
37
37
  if (typeof value === 'string') {
38
38
  try {
39
39
  const parsed = JSON.parse(value);
40
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
40
+ return isRecord(parsed) ? parsed : undefined;
41
41
  }
42
42
  catch {
43
43
  return undefined;
44
44
  }
45
45
  }
46
- return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
46
+ return isRecord(value) ? value : undefined;
47
47
  }
48
48
  const TRUTHY_STRINGS = new Set(['true', '1', 'yes', 'on']);
49
49
  const FALSY_STRINGS = new Set(['false', '0', 'no', 'off']);
50
- /** 按 OpenAPI schema 的基础类型对参数做轻量转换。 */
51
- function coerceValue(value, type, paramName) {
50
+ /**
51
+ * 按 OpenAPI schema 的基础类型对参数做轻量转换。
52
+ * @param fromData 值是否来自 `params.data`(影响数组字段是否包装对象)。
53
+ */
54
+ function coerceValue(value, type, paramName, fromData = false) {
52
55
  if (value === undefined || value === null)
53
56
  return value;
54
57
  if (type === 'number' || type === 'integer') {
@@ -75,50 +78,64 @@ function coerceValue(value, type, paramName) {
75
78
  }
76
79
  throw new ZentaoError('E_INVALID_PARAM', { param: paramName, value: String(value) });
77
80
  }
81
+ if (type === 'array') {
82
+ if (Array.isArray(value))
83
+ return value;
84
+ if (typeof value === 'string')
85
+ return value.split(',');
86
+ // 来自 params.data 的对象按原样保留,不强行包成单元素数组。
87
+ if (fromData && isRecord(value))
88
+ return value;
89
+ return [value];
90
+ }
78
91
  return value;
79
92
  }
80
- /** 将模块名、动作名和调用参数解析为实际 API 请求路径、查询参数和请求体。 */
81
- export function resolveModuleCommand(module, actionName, params = {}) {
82
- const action = getModuleAction(module.name, actionName);
83
- const pathValues = {};
93
+ /** 解析路径占位符的取值:scope 列表前缀、对象 ID,以及默认值补齐。 */
94
+ function resolvePathValues(module, action, params) {
95
+ const values = {};
84
96
  const pathParamNames = Object.keys(action.pathParams ?? {});
85
97
  // 生成定义中的 scope 列表接口会统一成 /{scope}/{scopeID}/xxx。
86
98
  if (pathParamNames.includes('scope')) {
87
99
  const scope = pickScope(params);
88
100
  if (!scope)
89
101
  throw new ZentaoError('E_MISSING_PARAM', { param: 'product/project/execution' });
90
- pathValues.scope = scope.scope;
91
- pathValues.scopeID = scope.scopeID;
102
+ values.scope = scope.scope;
103
+ values.scopeID = scope.scopeID;
92
104
  }
105
+ // 对象 ID 可来自 `id`、`{module}ID` 或路径中的 `xxxID` 占位符。
93
106
  const idParamName = pathParamNames.find((key) => key.endsWith('ID') && key !== 'scopeID');
94
107
  const idValue = params.id ?? params[`${module.name}ID`] ?? (idParamName ? params[idParamName] : undefined);
95
- const id = idValue === undefined ? undefined : Number(idValue);
96
- if (idParamName && id !== undefined && !Number.isNaN(id)) {
97
- pathValues[idParamName] = id;
108
+ const idNumber = idValue === undefined ? Number.NaN : Number(idValue);
109
+ const id = Number.isNaN(idNumber) ? undefined : idNumber;
110
+ if (idParamName && id !== undefined) {
111
+ values[idParamName] = id;
98
112
  }
99
113
  // pathParams 中未显式传值的参数,可从定义里的默认值或第一个可选项补齐。
100
114
  for (const key of pathParamNames) {
101
- if (key === 'scope' || key === 'scopeID' || pathValues[key] !== undefined)
115
+ if (key === 'scope' || key === 'scopeID' || values[key] !== undefined)
102
116
  continue;
103
117
  const definition = action.pathParams?.[key];
104
118
  const value = params[key];
105
119
  if (value !== undefined) {
106
- pathValues[key] = value;
120
+ values[key] = value;
107
121
  continue;
108
122
  }
109
123
  if (typeof definition === 'object') {
110
124
  if (definition.defaultValue !== undefined) {
111
- pathValues[key] = definition.defaultValue;
125
+ values[key] = definition.defaultValue;
112
126
  }
113
127
  else if (definition.options?.[0]?.value !== undefined) {
114
- pathValues[key] = definition.options[0].value;
128
+ values[key] = definition.options[0].value;
115
129
  }
116
130
  }
117
- if (pathValues[key] === undefined) {
131
+ if (values[key] === undefined) {
118
132
  throw new ZentaoError('E_MISSING_PARAM', { param: key });
119
133
  }
120
134
  }
121
- // 查询参数只从 action.params 中声明过的字段生成,避免把 body 字段误放到 URL 上。
135
+ return { values, id };
136
+ }
137
+ /** 仅从 action.params 中声明过的字段生成查询参数,避免把 body 字段误放到 URL 上。 */
138
+ function buildQuery(action, params) {
122
139
  const query = {};
123
140
  for (const param of action.params ?? []) {
124
141
  let value = params[param.name];
@@ -135,70 +152,72 @@ export function resolveModuleCommand(module, actionName, params = {}) {
135
152
  query[param.name] = value;
136
153
  }
137
154
  }
138
- let data = parseData(params.data);
139
- if (action.requestBody?.schema?.type === 'object') {
140
- data = data ? { ...data } : {};
141
- const schema = action.requestBody.schema;
142
- const required = new Set(schema.required ?? []);
143
- for (const [key, property] of Object.entries(schema.properties ?? {})) {
144
- // body 字段优先级:params.data 中的字段 > 平铺 params 字段 > schema 默认值。
145
- const hasDataValue = Object.prototype.hasOwnProperty.call(data, key);
146
- const hasParamValue = Object.prototype.hasOwnProperty.call(params, key);
147
- let value = hasDataValue ? data[key] : hasParamValue ? params[key] : property.defaultValue;
148
- if (value === undefined && (property.required || required.has(key))) {
149
- throw new ZentaoError('E_MISSING_PARAM', { param: key });
150
- }
151
- value = coerceValue(value, property.type, key);
152
- if (property.type === 'array' && value !== undefined && value !== null && !Array.isArray(value)) {
153
- if (typeof value === 'string') {
154
- value = value.split(',');
155
- }
156
- else if (!hasDataValue || !isRecord(value)) {
157
- value = [value];
158
- }
159
- }
160
- if (value !== undefined) {
161
- data[key] = value;
162
- }
155
+ return query;
156
+ }
157
+ /** 按 requestBody schema 组装请求体,并对各字段做取值优先级解析与类型转换。 */
158
+ function buildRequestBody(action, params) {
159
+ const base = parseData(params.data);
160
+ if (action.requestBody?.schema?.type !== 'object')
161
+ return base;
162
+ const data = base ? { ...base } : {};
163
+ const schema = action.requestBody.schema;
164
+ const required = new Set(schema.required ?? []);
165
+ for (const [key, property] of Object.entries(schema.properties ?? {})) {
166
+ // body 字段优先级:params.data 中的字段 > 平铺 params 字段 > schema 默认值。
167
+ const hasDataValue = Object.prototype.hasOwnProperty.call(data, key);
168
+ const hasParamValue = Object.prototype.hasOwnProperty.call(params, key);
169
+ const raw = hasDataValue ? data[key] : hasParamValue ? params[key] : property.defaultValue;
170
+ if (raw === undefined && (property.required || required.has(key))) {
171
+ throw new ZentaoError('E_MISSING_PARAM', { param: key });
172
+ }
173
+ const value = coerceValue(raw, property.type, key, hasDataValue);
174
+ if (value !== undefined) {
175
+ data[key] = value;
163
176
  }
164
177
  }
178
+ return data;
179
+ }
180
+ /** 将模块名、动作名和调用参数解析为实际 API 请求路径、查询参数和请求体。 */
181
+ export function resolveActionRequest(module, actionName, params = {}) {
182
+ const action = getModuleAction(module.name, actionName);
183
+ const { values, id } = resolvePathValues(module, action, params);
165
184
  return {
166
185
  module: module.name,
167
186
  action,
168
187
  params,
169
- path: resolvePath(action, pathValues),
170
- query,
171
- data,
172
- id: id === undefined || Number.isNaN(id) ? undefined : id,
188
+ path: resolvePath(action, values),
189
+ query: buildQuery(action, params),
190
+ data: buildRequestBody(action, params),
191
+ id,
173
192
  };
174
193
  }
175
194
  /** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
176
- export function extractResult(action, response) {
195
+ export function extractResult(action, response, params = {}) {
177
196
  const getter = action.resultGetter;
178
197
  if (!getter)
179
198
  return response.data ?? response;
180
199
  if (typeof getter === 'function')
181
- return getter(response, {});
200
+ return getter(response, params);
182
201
  if (typeof getter === 'string')
183
202
  return getNestedValue(response, getter);
184
203
  const result = {};
185
204
  for (const [targetKey, sourceKey] of Object.entries(getter)) {
186
- result[targetKey] = response[sourceKey];
205
+ result[targetKey] = getNestedValue(response, sourceKey);
187
206
  }
188
207
  return result;
189
208
  }
190
209
  /** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
191
- export function extractPager(action, response) {
210
+ export function extractPager(action, response, params = {}) {
192
211
  const getter = action.pagerGetter;
193
212
  if (!getter)
194
213
  return response.pager;
195
214
  if (typeof getter === 'function')
196
- return getter(response, {});
215
+ return getter(response, params);
197
216
  if (typeof getter === 'string')
198
217
  return getNestedValue(response, getter);
199
- const page = response[getter.pageID];
200
- const recPerPage = response[getter.recPerPage];
201
- const recTotal = response[getter.recTotal];
218
+ const page = getNestedValue(response, getter.pageID);
219
+ const recPerPage = getNestedValue(response, getter.recPerPage);
220
+ const recTotal = getNestedValue(response, getter.recTotal);
202
221
  if (page === undefined || recPerPage === undefined || recTotal === undefined)
203
222
  return undefined;
204
223
  return {
@@ -104,6 +104,9 @@ export type RequestResultFor<Name extends BuiltinRequestName> = ActionOfRequest<
104
104
  * 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
105
105
  * 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
106
106
  *
107
+ * 对 `update` 动作,当 `options.autoFill` 或全局 `autoFill` 为真时,会先 GET 当前对象,
108
+ * 用现值补齐用户未显式传入的 body 字段后再 PUT,避免禅道覆盖未提交字段。详见 {@link RequestOptions.autoFill}。
109
+ *
107
110
  * @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
108
111
  * @param name - 请求名,例如 `product`、`product/list` 或 `product/1`。
109
112
  * @param params - 请求参数。
@@ -1,7 +1,7 @@
1
1
  import { ZentaoError } from '../misc/errors.js';
2
2
  import { getGlobalOptions } from '../misc/global-options.js';
3
- import { getModule } from '../modules/registry.js';
4
- import { extractPager, extractResult, resolveModuleCommand } from '../modules/resolve.js';
3
+ import { getModule, getModuleAction } from '../modules/registry.js';
4
+ import { extractPager, extractResult, resolveActionRequest } from '../modules/resolve.js';
5
5
  import { isRecord, processData } from '../utils/index.js';
6
6
  /** 将 `moduleName`、`moduleName/methodName` 或 `moduleName/<objectID>` 请求名拆成模块名、动作名和对象 ID。 */
7
7
  function splitRequestName(name) {
@@ -30,6 +30,55 @@ function splitRequestName(name) {
30
30
  actionName,
31
31
  };
32
32
  }
33
+ /** 解析 `params.data` 中用户显式传入的 body 字段名,用于 autoFill 判断字段归属。 */
34
+ function getExplicitDataKeys(data) {
35
+ let value = data;
36
+ if (typeof value === 'string') {
37
+ try {
38
+ value = JSON.parse(value);
39
+ }
40
+ catch {
41
+ return new Set();
42
+ }
43
+ }
44
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
45
+ return new Set(Object.keys(value));
46
+ }
47
+ return new Set();
48
+ }
49
+ /**
50
+ * 在执行 `update` 动作前,用当前对象的现值填充用户未显式传入的 body 字段。
51
+ *
52
+ * 仅当模块存在 `type: 'get'` 动作且 update 动作声明了对象类型 body schema 时生效;
53
+ * 否则原样返回参数。GET 返回失败状态时会抛出 `E_API_FAILED`,避免继续发送未补齐的 PUT;
54
+ * GET 成功但返回非对象时跳过填充,交由后续 PUT 正常处理。
55
+ *
56
+ * 字段归属判断同时覆盖平铺 `params` 字段与 `params.data` 中的字段;只有 schema 中声明、
57
+ * 用户未传且当前对象存在的字段才会被补齐,避免覆盖用户本次想修改的字段。
58
+ */
59
+ async function autoFillUpdateParams(module, action, params, options) {
60
+ const properties = action.requestBody?.schema?.properties;
61
+ const getAction = module.actions.find((candidate) => candidate.type === 'get');
62
+ if (!properties || !getAction)
63
+ return params;
64
+ const current = (await request(`${module.name}/${getAction.name}`, params, {
65
+ client: options.client,
66
+ timeout: options.timeout,
67
+ insecure: options.insecure,
68
+ throwOnFail: true,
69
+ })).data;
70
+ if (!isRecord(current))
71
+ return params;
72
+ const explicitDataKeys = getExplicitDataKeys(params.data);
73
+ const filled = { ...params };
74
+ for (const key of Object.keys(properties)) {
75
+ const userProvided = Object.prototype.hasOwnProperty.call(params, key) || explicitDataKeys.has(key);
76
+ if (!userProvided && Object.prototype.hasOwnProperty.call(current, key)) {
77
+ filled[key] = current[key];
78
+ }
79
+ }
80
+ return filled;
81
+ }
33
82
  function stringifyMessage(value) {
34
83
  if (typeof value === 'string')
35
84
  return value;
@@ -96,9 +145,9 @@ function normalizeResponse(command, raw, options) {
96
145
  }
97
146
  const record = raw;
98
147
  const status = record.status === 'fail' ? 'fail' : 'success';
99
- const data = applyProcessing(extractResult(command.action, record), options);
148
+ const data = applyProcessing(extractResult(command.action, record, command.params), options);
100
149
  const rawMessage = record.message;
101
- const pager = extractPager(command.action, record);
150
+ const pager = extractPager(command.action, record, command.params);
102
151
  const response = {
103
152
  status,
104
153
  message: stringifyMessage(rawMessage),
@@ -134,7 +183,13 @@ export async function request(name, params = {}, options = {}) {
134
183
  ...(id !== undefined ? { id } : {}),
135
184
  ...(recPerPage !== undefined ? { recPerPage } : {}),
136
185
  };
137
- const command = resolveModuleCommand(module, actionName, mergedParams);
186
+ // autoFill:update 动作先 GET 当前对象,用现值补齐用户未显式传入的字段,
187
+ // 避免禅道 PUT 把未提交字段覆盖为空。
188
+ const action = getModuleAction(moduleName, actionName);
189
+ const finalParams = action.type === 'update' && (options.autoFill ?? globals.autoFill)
190
+ ? await autoFillUpdateParams(module, action, mergedParams, options)
191
+ : mergedParams;
192
+ const command = resolveActionRequest(module, actionName, finalParams);
138
193
  const raw = await client.request(command.path, {
139
194
  method: String(command.action.method).toUpperCase(),
140
195
  query: command.query,
@@ -142,6 +197,9 @@ export async function request(name, params = {}, options = {}) {
142
197
  timeout: options.timeout ?? globals.timeout,
143
198
  insecure: options.insecure ?? globals.insecure,
144
199
  });
200
+ if (options.raw) {
201
+ return raw;
202
+ }
145
203
  // limit 现归入本地处理选项;本次调用优先,缺省回落到全局默认。
146
204
  const processOptions = { ...options, limit: options.limit ?? globals.limit };
147
205
  const response = normalizeResponse(command, raw, processOptions);
@@ -11,10 +11,14 @@ export type ModuleActionParamOption = {
11
11
  readonly value: unknown;
12
12
  readonly label: string;
13
13
  };
14
+ /** 模块动作参数角色。 */
15
+ export type ModuleActionParamRole = 'query' | 'path' | 'body';
14
16
  /** 模块动作的查询参数定义。 */
15
17
  export interface ModuleActionParam {
16
18
  /** 参数名称。 */
17
19
  name: string;
20
+ /** 参数角色。 */
21
+ role?: ModuleActionParamRole;
18
22
  /** 参数说明。 */
19
23
  description?: string;
20
24
  /** 是否必填。 */
@@ -50,31 +54,42 @@ export interface ModuleActionResponse {
50
54
  /** 响应示例。 */
51
55
  example?: unknown;
52
56
  }
53
- /** 模块动作渲染目标类型;保留给 CLI 等上层应用使用。 */
54
- export type ModuleActionResultRenderType = 'markdown' | 'json' | 'raw';
55
- /** 模块动作自定义渲染函数类型;SDK 本身不直接渲染终端输出。 */
56
- export type ModuleActionResultRender = (result: unknown, type: ModuleActionResultRenderType, action: ModuleAction) => string;
57
- /** 从原始响应中提取分页字段时使用的字段映射。 */
57
+ /** 从原始响应中提取分页字段时使用的字段映射,值为原始响应中的字段路径(支持 `a.b` 嵌套)。 */
58
58
  export interface ModuleActionPagerGetterMap {
59
- /** 当前页码字段名。 */
59
+ /** 当前页码字段路径。 */
60
60
  pageID: string;
61
- /** 每页记录数字段名。 */
61
+ /** 每页记录数字段路径。 */
62
62
  recPerPage: string;
63
- /** 总记录数字段名。 */
63
+ /** 总记录数字段路径。 */
64
64
  recTotal: string;
65
65
  }
66
+ /**
67
+ * 从原始响应中重映射业务数据字段的映射表。
68
+ * 键为输出字段名,值为原始响应中的字段路径(支持 `a.b` 嵌套)。
69
+ */
70
+ export type ModuleActionResultFieldMap = Readonly<Record<string, string>>;
71
+ /**
72
+ * 从原始响应中提取数据时使用的函数形态。
73
+ * @param data 原始响应对象。
74
+ * @param params 触发本次请求的原始调用参数。
75
+ */
76
+ export type ModuleActionGetterFn<T> = (data: unknown, params: Record<string, unknown>) => T;
66
77
  /** 禅道模块中的单个 API 动作定义。 */
67
78
  export interface ModuleAction {
68
79
  /** 动作名称,例如 `list`、`get`、`close`。 */
69
80
  name: ModuleActionName;
70
- /** 动作类型,决定高阶 request 的路径/参数解析策略。 */
81
+ /** 动作类型,决定高阶 request 的路径/参数解析策略,并在 `method`、`resultType` 省略时作为推导依据。 */
71
82
  type: ModuleActionType;
72
83
  /** 面向用户展示的动作名称。 */
73
84
  display?: string;
74
85
  /** 动作说明。 */
75
86
  description?: string;
76
- /** HTTP 方法。 */
77
- method: ModuleActionMethod;
87
+ /**
88
+ * HTTP 方法;省略时按 {@link type} 自动推导:
89
+ * `list`/`get` → `GET`、`create`/`action` → `POST`、`update` → `PUT`、`delete` → `DELETE`。
90
+ * 当 `type` 无法推导出方法时抛出 `E_INDETERMINATE_ACTION_METHOD`。
91
+ */
92
+ method?: ModuleActionMethod;
78
93
  /** API 路径模板,可包含 `{productID}` 等路径参数。 */
79
94
  path: string;
80
95
  /** 路径参数定义;字符串为说明,对象可携带默认值和可选项。 */
@@ -83,14 +98,22 @@ export interface ModuleAction {
83
98
  params?: readonly ModuleActionParam[];
84
99
  /** 请求体定义。 */
85
100
  requestBody?: ModuleActionRequestBody;
86
- /** 结果形态。 */
87
- resultType: ModuleActionResultType;
88
- /** 从原始响应中提取分页信息的位置或函数。 */
89
- pagerGetter?: string | ModuleActionPagerGetterMap | ((data: unknown, params: Record<string, unknown>) => ListPagerInfo);
90
- /** 从原始响应中提取业务数据的位置或函数。 */
91
- resultGetter?: string | Record<string, string> | ((data: unknown, params: Record<string, unknown>) => unknown);
92
- /** 供上层应用使用的渲染配置。 */
93
- render?: string | ModuleActionResultRender | Record<ModuleActionResultRenderType, ModuleActionResultRender>;
101
+ /**
102
+ * 结果形态;省略时按 {@link type} 自动推导:
103
+ * `list` → `list`、`get`/`create`/`update` → `object`、`delete`/`action` → `text`。
104
+ * 当 `type` 无法推导出结果形态时抛出 `E_INDETERMINATE_ACTION_RESULT_TYPE`。
105
+ */
106
+ resultType?: ModuleActionResultType;
107
+ /**
108
+ * 从原始响应中提取分页信息的位置或函数:
109
+ * 字符串为字段路径(支持 `a.b` 嵌套)、对象为字段映射、函数则接收原始响应与调用参数。
110
+ */
111
+ pagerGetter?: string | ModuleActionPagerGetterMap | ModuleActionGetterFn<ListPagerInfo>;
112
+ /**
113
+ * 从原始响应中提取业务数据的位置或函数:
114
+ * 字符串为字段路径(支持 `a.b` 嵌套)、对象为字段映射、函数则接收原始响应与调用参数。
115
+ */
116
+ resultGetter?: string | ModuleActionResultFieldMap | ModuleActionGetterFn<unknown>;
94
117
  }
95
118
  /** 内置模块名称,同时允许用户扩展自定义模块名。 */
96
119
  export type ModuleName = 'user' | 'program' | 'product' | 'project' | 'execution' | 'productplan' | 'story' | 'epic' | 'requirement' | 'bug' | 'testcase' | 'task' | 'feedback' | 'ticket' | 'system' | 'build' | 'testtask' | 'release' | 'file' | (string & {});
@@ -106,7 +129,7 @@ export interface ModuleDefinition {
106
129
  actions: readonly ModuleAction[];
107
130
  }
108
131
  /** 将模块动作和参数解析后的可执行请求描述。 */
109
- export interface ResolvedModuleCommand {
132
+ export interface ModuleActionRequest {
110
133
  /** 模块名称。 */
111
134
  module: string;
112
135
  /** 匹配到的动作定义。 */
@@ -16,6 +16,12 @@ export interface GlobalOptions {
16
16
  persistProfiles?: boolean;
17
17
  /** 当禅道服务端返回 `{ status: "fail" }` 时是否抛出 `E_API_FAILED`,默认 false。 */
18
18
  throwOnFail?: boolean;
19
+ /**
20
+ * 是否在执行 `update` 操作时自动填充未传入的字段,默认 false。
21
+ *
22
+ * 优先级低于单次请求选项;语义见 {@link RequestOptions.autoFill}。
23
+ */
24
+ autoFill?: boolean;
19
25
  }
20
26
  /** 高阶 `request("moduleName")` / `request("moduleName/methodName")` / `request("moduleName/<objectID>")` 的单次调用选项。 */
21
27
  export interface RequestOptions extends ProcessListOptions {
@@ -32,4 +38,16 @@ export interface RequestOptions extends ProcessListOptions {
32
38
  * 不传时回落到全局 `throwOnFail`,默认 false(保留原始失败响应)。
33
39
  */
34
40
  throwOnFail?: boolean;
41
+ /**
42
+ * 是否在执行 `update` 操作时自动填充未传入的字段。
43
+ *
44
+ * 设为 `true` 后,会先 GET 当前对象,把用户未显式传入(含 `params.data`)且
45
+ * 动作 body schema 中声明的字段用现值补齐,再发起 PUT,避免禅道用空值覆盖未提交字段。
46
+ * 因此只需传想修改的字段即可。仅对 `type: 'update'` 且模块存在 `type: 'get'` 动作时生效。
47
+ *
48
+ * 不传时回落到全局 `autoFill`,默认 false。
49
+ */
50
+ autoFill?: boolean;
51
+ /** 是否返回原始响应体,默认 false。 */
52
+ raw?: boolean;
35
53
  }
@@ -1,4 +1,4 @@
1
- export { isRecord, getNestedValue } from './object.js';
1
+ export { isRecord, isBlank, getNestedValue } from './object.js';
2
2
  export { asArray } from './array.js';
3
3
  export { normalizeSiteUrl } from './url.js';
4
4
  export { pickFields, pickFieldsSingle, filterData, searchData, sortData, processData, } from './data.js';
@@ -1,4 +1,4 @@
1
- export { isRecord, getNestedValue } from './object.js';
1
+ export { isRecord, isBlank, getNestedValue } from './object.js';
2
2
  export { asArray } from './array.js';
3
3
  export { normalizeSiteUrl } from './url.js';
4
4
  export { pickFields, pickFieldsSingle, filterData, searchData, sortData, processData, } from './data.js';
@@ -1,3 +1,5 @@
1
1
  /** 判断值是否为普通对象(非数组、非 null)。 */
2
2
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ /** 判断值是否为“空”(undefined、null 或空字符串)。 */
4
+ export declare function isBlank(value: unknown): boolean;
3
5
  export declare function getNestedValue(obj: unknown, path: string): unknown;
@@ -2,6 +2,10 @@
2
2
  export function isRecord(value) {
3
3
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
4
4
  }
5
+ /** 判断值是否为“空”(undefined、null 或空字符串)。 */
6
+ export function isBlank(value) {
7
+ return value === undefined || value === null || value === '';
8
+ }
5
9
  export function getNestedValue(obj, path) {
6
10
  const keys = path.split('.');
7
11
  let current = obj;
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
- const fallbackBuild = "2026-06-29T10:27:50.494Z";
2
- const fallbackVersion = "0.3.2";
1
+ const fallbackBuild = "2026-07-21T02:40:34.356Z";
2
+ const fallbackVersion = "0.4.0";
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.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Browser and Node.js SDK for ZenTao API",
6
6
  "license": "MIT",