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.
- package/dist/browser/zentao-api.global.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/misc/errors.d.ts +2 -0
- package/dist/misc/errors.js +2 -0
- package/dist/modules/define.d.ts +8 -4
- package/dist/modules/define.js +8 -4
- package/dist/modules/generated.d.ts +26 -120
- package/dist/modules/generated.js +26 -72
- package/dist/modules/query.d.ts +13 -1
- package/dist/modules/query.js +59 -0
- package/dist/modules/registry-store.d.ts +8 -0
- package/dist/modules/registry-store.js +50 -2
- package/dist/modules/registry.d.ts +27 -121
- package/dist/modules/registry.js +1 -1
- package/dist/modules/resolve.d.ts +4 -4
- package/dist/modules/resolve.js +78 -59
- package/dist/request/index.d.ts +3 -0
- package/dist/request/index.js +63 -5
- package/dist/types/module.d.ts +43 -20
- package/dist/types/options.d.ts +18 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/object.d.ts +2 -0
- package/dist/utils/object.js +4 -0
- package/dist/version.js +2 -2
- package/package.json +1 -1
package/dist/modules/resolve.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
40
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
41
41
|
}
|
|
42
42
|
catch {
|
|
43
43
|
return undefined;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
return
|
|
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
|
-
/**
|
|
51
|
-
|
|
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
|
-
/**
|
|
81
|
-
|
|
82
|
-
const
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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' ||
|
|
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
|
-
|
|
120
|
+
values[key] = value;
|
|
107
121
|
continue;
|
|
108
122
|
}
|
|
109
123
|
if (typeof definition === 'object') {
|
|
110
124
|
if (definition.defaultValue !== undefined) {
|
|
111
|
-
|
|
125
|
+
values[key] = definition.defaultValue;
|
|
112
126
|
}
|
|
113
127
|
else if (definition.options?.[0]?.value !== undefined) {
|
|
114
|
-
|
|
128
|
+
values[key] = definition.options[0].value;
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
|
-
if (
|
|
131
|
+
if (values[key] === undefined) {
|
|
118
132
|
throw new ZentaoError('E_MISSING_PARAM', { param: key });
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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,
|
|
170
|
-
query,
|
|
171
|
-
data,
|
|
172
|
-
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
|
|
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
|
|
200
|
-
const recPerPage = response
|
|
201
|
-
const recTotal = response
|
|
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 {
|
package/dist/request/index.d.ts
CHANGED
|
@@ -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 - 请求参数。
|
package/dist/request/index.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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);
|
package/dist/types/module.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
77
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
|
|
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
|
|
132
|
+
export interface ModuleActionRequest {
|
|
110
133
|
/** 模块名称。 */
|
|
111
134
|
module: string;
|
|
112
135
|
/** 匹配到的动作定义。 */
|
package/dist/types/options.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/utils/index.js
CHANGED
|
@@ -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';
|
package/dist/utils/object.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/object.js
CHANGED
|
@@ -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