zentao-api 0.3.1 → 0.3.3
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/README.md +38 -2
- 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 +67 -0
- package/dist/modules/define.js +123 -0
- package/dist/modules/generated.d.ts +0 -38
- package/dist/modules/generated.js +0 -38
- package/dist/modules/override.d.ts +60 -0
- package/dist/modules/override.js +113 -0
- package/dist/modules/query.d.ts +44 -0
- package/dist/modules/query.js +68 -0
- package/dist/modules/registry-store.d.ts +39 -0
- package/dist/modules/registry-store.js +181 -0
- package/dist/modules/registry.d.ts +4 -120
- package/dist/modules/registry.js +16 -221
- package/dist/modules/resolve.d.ts +2 -2
- package/dist/modules/resolve.js +8 -8
- package/dist/request/index.d.ts +3 -0
- package/dist/request/index.js +58 -4
- package/dist/types/client.d.ts +38 -0
- package/dist/types/client.js +1 -0
- package/dist/types/data.d.ts +42 -0
- package/dist/types/data.js +1 -0
- package/dist/types/index.d.ts +17 -359
- package/dist/types/index.js +17 -1
- package/dist/types/module.d.ts +143 -0
- package/dist/types/module.js +1 -0
- package/dist/types/options.d.ts +51 -0
- package/dist/types/options.js +1 -0
- package/dist/types/profile.d.ts +54 -0
- package/dist/types/profile.js +1 -0
- package/dist/types/response.d.ts +70 -0
- package/dist/types/response.js +1 -0
- package/dist/version.js +2 -2
- package/package.json +1 -1
package/dist/modules/registry.js
CHANGED
|
@@ -1,224 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// 模块注册表的统一出入口(barrel)。
|
|
2
|
+
// 实际实现按职责拆分:
|
|
3
|
+
// - ./registry-store —— 共享运行时状态与 克隆/冻结/合并/校验 等底层原语(内部使用)。
|
|
4
|
+
// - ./define —— 定义/写入模块(defineModules / defineModuleActions / resetModuleDefinitions)。
|
|
5
|
+
// - ./override —— 内置覆盖/扩展(基于 define,随 SDK 发布并在加载/重置时自动应用)。
|
|
6
|
+
// - ./query —— 获取模块信息(getModule / getModuleAction / getModuleNames / isModuleName)。
|
|
3
7
|
import { BUILTIN_MODULES } from './generated.js';
|
|
8
|
+
import { applyBuiltinOverrides } from './override.js';
|
|
9
|
+
import { setPostResetHook } from './registry-store.js';
|
|
4
10
|
export { BUILTIN_MODULES };
|
|
5
11
|
export const MODULES = BUILTIN_MODULES;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return value.map((item) => deepClone(item));
|
|
15
|
-
}
|
|
16
|
-
if (value && typeof value === 'object' && !(value instanceof Function)) {
|
|
17
|
-
const result = {};
|
|
18
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
19
|
-
result[key] = deepClone(nestedValue);
|
|
20
|
-
}
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
function cloneBuiltinModules() {
|
|
26
|
-
return deepClone(BUILTIN_MODULES);
|
|
27
|
-
}
|
|
28
|
-
function deepFreeze(value) {
|
|
29
|
-
if (value === null || typeof value !== 'object')
|
|
30
|
-
return value;
|
|
31
|
-
if (Object.isFrozen(value))
|
|
32
|
-
return value;
|
|
33
|
-
for (const key of Object.keys(value)) {
|
|
34
|
-
deepFreeze(value[key]);
|
|
35
|
-
}
|
|
36
|
-
return Object.freeze(value);
|
|
37
|
-
}
|
|
38
|
-
function freezeAction(action) {
|
|
39
|
-
return deepFreeze(action);
|
|
40
|
-
}
|
|
41
|
-
function freezeModule(module) {
|
|
42
|
-
module.actions.forEach(freezeAction);
|
|
43
|
-
return deepFreeze(module);
|
|
44
|
-
}
|
|
45
|
-
function freezeModules(source) {
|
|
46
|
-
source.forEach(freezeModule);
|
|
47
|
-
return source;
|
|
48
|
-
}
|
|
49
|
-
function findActionIndex(source, actionName) {
|
|
50
|
-
const key = actionName.toLowerCase();
|
|
51
|
-
return source.findIndex((action) => String(action.name).toLowerCase() === key);
|
|
52
|
-
}
|
|
53
|
-
function mergeActions(base, extension) {
|
|
54
|
-
const next = base.slice();
|
|
55
|
-
for (const action of extension) {
|
|
56
|
-
const index = findActionIndex(next, String(action.name));
|
|
57
|
-
const frozen = freezeAction(deepClone(action));
|
|
58
|
-
if (index >= 0) {
|
|
59
|
-
next[index] = frozen;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
next.push(frozen);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return next;
|
|
66
|
-
}
|
|
67
|
-
function mergeModule(base, extension) {
|
|
68
|
-
return freezeModule({
|
|
69
|
-
...base,
|
|
70
|
-
...deepClone(extension),
|
|
71
|
-
actions: mergeActions(base.actions, extension.actions),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
function buildModuleMap(source) {
|
|
75
|
-
return new Map(source.map((module) => [module.name.toLowerCase(), module]));
|
|
76
|
-
}
|
|
77
|
-
function rebuildMap() {
|
|
78
|
-
moduleMap = buildModuleMap(modules);
|
|
79
|
-
}
|
|
80
|
-
function validateModule(module) {
|
|
81
|
-
if (!module || typeof module.name !== 'string' || !Array.isArray(module.actions)) {
|
|
82
|
-
throw new ZentaoError('E_INVALID_MODULE_DEFINITION');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function validateAction(action) {
|
|
86
|
-
if (!action || typeof action.name !== 'string' || typeof action.path !== 'string' || typeof action.method !== 'string') {
|
|
87
|
-
throw new ZentaoError('E_INVALID_ACTION_DEFINITION');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* 注册或扩展模块定义。
|
|
92
|
-
*
|
|
93
|
-
* 行为细节:
|
|
94
|
-
* - 模块名匹配大小写不敏感。
|
|
95
|
-
* - 未知模块直接追加到注册表末尾。
|
|
96
|
-
* - 已存在的模块默认按 `mergeModule` 合并:模块元数据浅合并、动作按名同名替换/未知追加;
|
|
97
|
-
* `options.replace` 为 `true` 时整体替换。
|
|
98
|
-
* - 所有写入都会做深克隆 + 深冻结:调用方后续修改自己的对象不会污染注册表,注册表也不可被外部改写。
|
|
99
|
-
*
|
|
100
|
-
* @param input - 单个或一组模块定义。
|
|
101
|
-
* @param options - 写入策略,参见 {@link DefineModulesOptions}。
|
|
102
|
-
* @throws {ZentaoError} `E_INVALID_MODULE_DEFINITION` —— 缺少 `name` 或 `actions` 字段。
|
|
103
|
-
*/
|
|
104
|
-
export function defineModules(input, options = {}) {
|
|
105
|
-
for (const module of asArray(input)) {
|
|
106
|
-
validateModule(module);
|
|
107
|
-
const key = module.name.toLowerCase();
|
|
108
|
-
const index = modules.findIndex((item) => item.name.toLowerCase() === key);
|
|
109
|
-
if (index >= 0) {
|
|
110
|
-
modules[index] = options.replace
|
|
111
|
-
? freezeModule(deepClone(module))
|
|
112
|
-
: mergeModule(modules[index], module);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
modules.push(freezeModule(deepClone(module)));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
rebuildMap();
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* 为已存在的模块追加或覆盖动作。
|
|
122
|
-
*
|
|
123
|
-
* 不做深度合并:同名动作整体替换,未知动作追加。这避免在 schema、参数数组等字段上出现隐式合并规则。
|
|
124
|
-
*
|
|
125
|
-
* @param moduleName - 目标模块名(大小写不敏感)。
|
|
126
|
-
* @param input - 单个或一组动作定义。
|
|
127
|
-
* @throws {ZentaoError} `E_INVALID_MODULE`(模块未注册)或 `E_INVALID_ACTION_DEFINITION`
|
|
128
|
-
* (动作缺少 `name` / `path` / `method` 等必填字段)。
|
|
129
|
-
*/
|
|
130
|
-
export function defineModuleActions(moduleName, input) {
|
|
131
|
-
const key = moduleName.toLowerCase();
|
|
132
|
-
const module = moduleMap.get(key);
|
|
133
|
-
if (!module) {
|
|
134
|
-
throw new ZentaoError('E_INVALID_MODULE', { module: moduleName });
|
|
135
|
-
}
|
|
136
|
-
const actions = module.actions.slice();
|
|
137
|
-
for (const action of asArray(input)) {
|
|
138
|
-
validateAction(action);
|
|
139
|
-
const index = findActionIndex(actions, String(action.name));
|
|
140
|
-
const frozen = freezeAction(deepClone(action));
|
|
141
|
-
// 同名动作替换,未知动作追加;不做深度合并,避免 schema/数组字段出现隐式规则。
|
|
142
|
-
if (index >= 0) {
|
|
143
|
-
actions[index] = frozen;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
actions.push(frozen);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const nextModule = freezeModule({ ...module, actions });
|
|
150
|
-
const index = modules.findIndex((item) => item.name.toLowerCase() === key);
|
|
151
|
-
modules[index] = nextModule;
|
|
152
|
-
rebuildMap();
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* 获取模块定义。
|
|
156
|
-
*
|
|
157
|
-
* 模块名匹配大小写不敏感。返回值是注册表内部的已深冻结引用(O(1) 查询、零拷贝),
|
|
158
|
-
* 任何写入尝试在严格模式下会抛 `TypeError`;如需修改请使用 {@link defineModules}。
|
|
159
|
-
*
|
|
160
|
-
* @param moduleName - 模块名。
|
|
161
|
-
* @returns 已注册的模块定义。
|
|
162
|
-
* @throws {ZentaoError} `E_INVALID_MODULE` —— 模块未注册。
|
|
163
|
-
*/
|
|
164
|
-
export function getModule(moduleName) {
|
|
165
|
-
const module = moduleMap.get(moduleName.toLowerCase());
|
|
166
|
-
if (!module) {
|
|
167
|
-
throw new ZentaoError('E_INVALID_MODULE', { module: moduleName });
|
|
168
|
-
}
|
|
169
|
-
return module;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* 获取指定模块下的某个动作。
|
|
173
|
-
*
|
|
174
|
-
* 解析顺序:
|
|
175
|
-
* 1. `actionName === 'ls'` 时映射为 `list`(仅作为别名,不会修改注册表)。
|
|
176
|
-
* 2. 在该模块的动作中按名称大小写不敏感匹配。
|
|
177
|
-
* 3. 当请求的动作不是基础 CRUD(`list`/`get`/`create`/`update`/`delete`)时,
|
|
178
|
-
* 额外允许命中 `type === 'action'` 的自定义动作(即使名字不在基础 CRUD 中)。
|
|
179
|
-
*
|
|
180
|
-
* 返回值同样是已深冻结的引用,请勿尝试修改。
|
|
181
|
-
*
|
|
182
|
-
* @param moduleName - 模块名(大小写不敏感)。
|
|
183
|
-
* @param actionName - 动作名(大小写不敏感);支持 `ls` 作为 `list` 的别名。
|
|
184
|
-
* @returns 匹配到的动作定义。
|
|
185
|
-
* @throws {ZentaoError} `E_INVALID_MODULE`(模块未注册)或 `E_INVALID_ACTION`(动作不存在)。
|
|
186
|
-
*/
|
|
187
|
-
export function getModuleAction(moduleName, actionName) {
|
|
188
|
-
const module = getModule(moduleName);
|
|
189
|
-
const normalized = actionName === 'ls' ? 'list' : actionName;
|
|
190
|
-
const direct = module.actions.find((action) => String(action.name).toLowerCase() === normalized.toLowerCase());
|
|
191
|
-
if (direct)
|
|
192
|
-
return direct;
|
|
193
|
-
const crud = new Set(['list', 'get', 'create', 'update', 'delete']);
|
|
194
|
-
if (!crud.has(normalized)) {
|
|
195
|
-
const custom = module.actions.find((action) => action.type === 'action' && String(action.name).toLowerCase() === normalized.toLowerCase());
|
|
196
|
-
if (custom)
|
|
197
|
-
return custom;
|
|
198
|
-
}
|
|
199
|
-
throw new ZentaoError('E_INVALID_ACTION', { module: moduleName, action: actionName });
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* 返回当前运行时注册表中的所有模块名。
|
|
203
|
-
*
|
|
204
|
-
* 顺序与模块写入注册表的顺序一致;包括内置模块和通过 {@link defineModules} 追加的用户模块。
|
|
205
|
-
*
|
|
206
|
-
* @returns 模块名数组(保留原始大小写)。
|
|
207
|
-
*/
|
|
208
|
-
export function getModuleNames() {
|
|
209
|
-
return modules.map((module) => module.name);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* 判断模块名是否已注册。
|
|
213
|
-
*
|
|
214
|
-
* @param moduleName - 模块名;匹配大小写不敏感。
|
|
215
|
-
* @returns 已注册返回 `true`,否则 `false`。
|
|
216
|
-
*/
|
|
217
|
-
export function isModuleName(moduleName) {
|
|
218
|
-
return moduleMap.has(moduleName.toLowerCase());
|
|
219
|
-
}
|
|
220
|
-
/** @internal */
|
|
221
|
-
export function resetModuleDefinitions() {
|
|
222
|
-
modules = freezeModules(cloneBuiltinModules());
|
|
223
|
-
rebuildMap();
|
|
224
|
-
}
|
|
12
|
+
export { defineModules, defineModuleActions, extendModuleAction, resetModuleDefinitions, } from './define.js';
|
|
13
|
+
export { applyBuiltinOverrides };
|
|
14
|
+
// 内置覆盖接线:避免 define ↔ override 之间的循环依赖。
|
|
15
|
+
// - 加载时应用一次;
|
|
16
|
+
// - 注册为 store 的「重置后钩子」,使 resetModuleDefinitions 还原内置基线后自动重新应用。
|
|
17
|
+
setPostResetHook(applyBuiltinOverrides);
|
|
18
|
+
applyBuiltinOverrides();
|
|
19
|
+
export { getModule, getModuleAction, getModuleNames, isModuleName, } from './query.js';
|
|
@@ -2,6 +2,6 @@ import type { ListPagerInfo, ModuleAction, ModuleDefinition, ResolvedModuleComma
|
|
|
2
2
|
/** 将模块名、动作名和调用参数解析为实际 API 请求路径、查询参数和请求体。 */
|
|
3
3
|
export declare function resolveModuleCommand(module: ModuleDefinition, actionName: string, params?: Record<string, unknown>): ResolvedModuleCommand;
|
|
4
4
|
/** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
|
|
5
|
-
export declare function extractResult(action: ModuleAction, response: Record<string, unknown>): unknown;
|
|
5
|
+
export declare function extractResult(action: ModuleAction, response: Record<string, unknown>, params?: Record<string, unknown>): unknown;
|
|
6
6
|
/** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
|
|
7
|
-
export declare function extractPager(action: ModuleAction, response: Record<string, unknown>): ListPagerInfo | undefined;
|
|
7
|
+
export declare function extractPager(action: ModuleAction, response: Record<string, unknown>, params?: Record<string, unknown>): ListPagerInfo | undefined;
|
package/dist/modules/resolve.js
CHANGED
|
@@ -173,32 +173,32 @@ export function resolveModuleCommand(module, actionName, params = {}) {
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
/** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
|
|
176
|
-
export function extractResult(action, response) {
|
|
176
|
+
export function extractResult(action, response, params = {}) {
|
|
177
177
|
const getter = action.resultGetter;
|
|
178
178
|
if (!getter)
|
|
179
179
|
return response.data ?? response;
|
|
180
180
|
if (typeof getter === 'function')
|
|
181
|
-
return getter(response,
|
|
181
|
+
return getter(response, params);
|
|
182
182
|
if (typeof getter === 'string')
|
|
183
183
|
return getNestedValue(response, getter);
|
|
184
184
|
const result = {};
|
|
185
185
|
for (const [targetKey, sourceKey] of Object.entries(getter)) {
|
|
186
|
-
result[targetKey] = response
|
|
186
|
+
result[targetKey] = getNestedValue(response, sourceKey);
|
|
187
187
|
}
|
|
188
188
|
return result;
|
|
189
189
|
}
|
|
190
190
|
/** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
|
|
191
|
-
export function extractPager(action, response) {
|
|
191
|
+
export function extractPager(action, response, params = {}) {
|
|
192
192
|
const getter = action.pagerGetter;
|
|
193
193
|
if (!getter)
|
|
194
194
|
return response.pager;
|
|
195
195
|
if (typeof getter === 'function')
|
|
196
|
-
return getter(response,
|
|
196
|
+
return getter(response, params);
|
|
197
197
|
if (typeof getter === 'string')
|
|
198
198
|
return getNestedValue(response, getter);
|
|
199
|
-
const page = response
|
|
200
|
-
const recPerPage = response
|
|
201
|
-
const recTotal = response
|
|
199
|
+
const page = getNestedValue(response, getter.pageID);
|
|
200
|
+
const recPerPage = getNestedValue(response, getter.recPerPage);
|
|
201
|
+
const recTotal = getNestedValue(response, getter.recTotal);
|
|
202
202
|
if (page === undefined || recPerPage === undefined || recTotal === undefined)
|
|
203
203
|
return undefined;
|
|
204
204
|
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,6 +1,6 @@
|
|
|
1
1
|
import { ZentaoError } from '../misc/errors.js';
|
|
2
2
|
import { getGlobalOptions } from '../misc/global-options.js';
|
|
3
|
-
import { getModule } from '../modules/registry.js';
|
|
3
|
+
import { getModule, getModuleAction } from '../modules/registry.js';
|
|
4
4
|
import { extractPager, extractResult, resolveModuleCommand } from '../modules/resolve.js';
|
|
5
5
|
import { isRecord, processData } from '../utils/index.js';
|
|
6
6
|
/** 将 `moduleName`、`moduleName/methodName` 或 `moduleName/<objectID>` 请求名拆成模块名、动作名和对象 ID。 */
|
|
@@ -30,6 +30,54 @@ 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 失败或返回非对象时同样跳过填充,交由后续 PUT 正常处理。
|
|
54
|
+
*
|
|
55
|
+
* 字段归属判断同时覆盖平铺 `params` 字段与 `params.data` 中的字段;只有 schema 中声明、
|
|
56
|
+
* 用户未传且当前对象存在的字段才会被补齐,避免覆盖用户本次想修改的字段。
|
|
57
|
+
*/
|
|
58
|
+
async function autoFillUpdateParams(module, action, params, options) {
|
|
59
|
+
const properties = action.requestBody?.schema?.properties;
|
|
60
|
+
const getAction = module.actions.find((candidate) => candidate.type === 'get');
|
|
61
|
+
if (!properties || !getAction)
|
|
62
|
+
return params;
|
|
63
|
+
const current = (await request(`${module.name}/${getAction.name}`, params, {
|
|
64
|
+
client: options.client,
|
|
65
|
+
timeout: options.timeout,
|
|
66
|
+
insecure: options.insecure,
|
|
67
|
+
throwOnFail: options.throwOnFail,
|
|
68
|
+
})).data;
|
|
69
|
+
if (!isRecord(current))
|
|
70
|
+
return params;
|
|
71
|
+
const explicitDataKeys = getExplicitDataKeys(params.data);
|
|
72
|
+
const filled = { ...params };
|
|
73
|
+
for (const key of Object.keys(properties)) {
|
|
74
|
+
const userProvided = Object.prototype.hasOwnProperty.call(params, key) || explicitDataKeys.has(key);
|
|
75
|
+
if (!userProvided && Object.prototype.hasOwnProperty.call(current, key)) {
|
|
76
|
+
filled[key] = current[key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return filled;
|
|
80
|
+
}
|
|
33
81
|
function stringifyMessage(value) {
|
|
34
82
|
if (typeof value === 'string')
|
|
35
83
|
return value;
|
|
@@ -96,9 +144,9 @@ function normalizeResponse(command, raw, options) {
|
|
|
96
144
|
}
|
|
97
145
|
const record = raw;
|
|
98
146
|
const status = record.status === 'fail' ? 'fail' : 'success';
|
|
99
|
-
const data = applyProcessing(extractResult(command.action, record), options);
|
|
147
|
+
const data = applyProcessing(extractResult(command.action, record, command.params), options);
|
|
100
148
|
const rawMessage = record.message;
|
|
101
|
-
const pager = extractPager(command.action, record);
|
|
149
|
+
const pager = extractPager(command.action, record, command.params);
|
|
102
150
|
const response = {
|
|
103
151
|
status,
|
|
104
152
|
message: stringifyMessage(rawMessage),
|
|
@@ -134,7 +182,13 @@ export async function request(name, params = {}, options = {}) {
|
|
|
134
182
|
...(id !== undefined ? { id } : {}),
|
|
135
183
|
...(recPerPage !== undefined ? { recPerPage } : {}),
|
|
136
184
|
};
|
|
137
|
-
|
|
185
|
+
// autoFill:update 动作先 GET 当前对象,用现值补齐用户未显式传入的字段,
|
|
186
|
+
// 避免禅道 PUT 把未提交字段覆盖为空。
|
|
187
|
+
const action = getModuleAction(moduleName, actionName);
|
|
188
|
+
const finalParams = action.type === 'update' && (options.autoFill ?? globals.autoFill)
|
|
189
|
+
? await autoFillUpdateParams(module, action, mergedParams, options)
|
|
190
|
+
: mergedParams;
|
|
191
|
+
const command = resolveModuleCommand(module, actionName, finalParams);
|
|
138
192
|
const raw = await client.request(command.path, {
|
|
139
193
|
method: String(command.action.method).toUpperCase(),
|
|
140
194
|
query: command.query,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** 创建 {@link ZentaoClient} 时使用的配置。 */
|
|
2
|
+
export interface ZentaoClientOptions {
|
|
3
|
+
/** 禅道站点根地址,例如 `https://zentao.example.com`;SDK 会自动拼接 `/api.php/v2`。 */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
/** 禅道 API Token;未提供时可稍后通过 {@link ZentaoClient.login} 获取并写入实例。 */
|
|
6
|
+
token?: string;
|
|
7
|
+
/** 默认请求超时时间,单位毫秒。 */
|
|
8
|
+
timeout?: number;
|
|
9
|
+
/** 是否跳过 TLS 证书验证;仅 Node.js 运行时支持,浏览器中会抛错。 */
|
|
10
|
+
insecure?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** SDK 支持的 HTTP 方法。 */
|
|
13
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
14
|
+
/** 请求体序列化方式。 */
|
|
15
|
+
export type ClientRequestBodyType = 'json' | 'form' | 'raw';
|
|
16
|
+
/** 响应体解析方式。 */
|
|
17
|
+
export type ClientResponseType = 'auto' | 'json' | 'text' | 'arrayBuffer' | 'blob' | 'response';
|
|
18
|
+
/** `ZentaoClient.request()` 的单次请求选项。 */
|
|
19
|
+
export interface ClientRequestOptions {
|
|
20
|
+
/** HTTP 方法,默认 `GET`。 */
|
|
21
|
+
method?: HttpMethod;
|
|
22
|
+
/** 请求体;`GET` 请求会忽略该字段。普通对象默认按 JSON 发送,`FormData` / `Blob` / `ArrayBuffer` 等会原样发送。 */
|
|
23
|
+
body?: unknown;
|
|
24
|
+
/** 请求体序列化方式。默认 `json`;传入 `FormData` 等原生 body 时会自动按 `raw` 处理。 */
|
|
25
|
+
bodyType?: ClientRequestBodyType;
|
|
26
|
+
/** 响应体解析方式。默认 `auto`,会优先尝试 JSON,失败后回落为文本。 */
|
|
27
|
+
responseType?: ClientResponseType;
|
|
28
|
+
/** 额外请求头;会与 SDK 自动注入的 `Token` / `Content-Type` 合并。 */
|
|
29
|
+
headers?: HeadersInit;
|
|
30
|
+
/** URL 查询参数;`undefined` 值会被跳过。 */
|
|
31
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
32
|
+
/** 外部取消信号;会与 SDK 自身的超时控制合并。 */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** 单次请求超时时间,优先级高于全局和客户端默认值。 */
|
|
35
|
+
timeout?: number;
|
|
36
|
+
/** 单次请求 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
37
|
+
insecure?: boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** 本地数据处理的基础记录类型,对应一条对象数据。 */
|
|
2
|
+
export type DataRecord = Record<string, unknown>;
|
|
3
|
+
/** 单条过滤条件,字段名支持 `.` 访问子字段。 */
|
|
4
|
+
export interface DataRecordFilter {
|
|
5
|
+
/** 字段路径,例如 `status` 或 `assignedTo.id`。 */
|
|
6
|
+
key: string;
|
|
7
|
+
/** 比较运算符。 */
|
|
8
|
+
operator: '=' | '!=' | '>' | '<' | '>=' | '<=' | '~' | '!~';
|
|
9
|
+
/** 比较值;数组用于 `=`/`!=`/`~`/`!~` 的“任一/全不”匹配。 */
|
|
10
|
+
value: string | number | boolean | string[];
|
|
11
|
+
}
|
|
12
|
+
/** 一组过滤条件,组内按 `operator` 组合;多组之间按 AND 组合。 */
|
|
13
|
+
export interface DataRecordFilterGroup {
|
|
14
|
+
/** 组内条件的组合方式。 */
|
|
15
|
+
operator: 'AND' | 'OR';
|
|
16
|
+
/** 组内条件列表。 */
|
|
17
|
+
conditions: DataRecordFilter[];
|
|
18
|
+
}
|
|
19
|
+
/** 排序表达式,格式为 `字段:asc|desc`。 */
|
|
20
|
+
export type SortExpr = `${string}:${'asc' | 'desc'}`;
|
|
21
|
+
/** 自定义排序比较函数。 */
|
|
22
|
+
export type SortFn = (a: DataRecord, b: DataRecord) => number;
|
|
23
|
+
/** {@link processData} 处理列表时的选项;执行顺序为 过滤 → 搜索 → 排序 → 限制数量 → 摘取。 */
|
|
24
|
+
export interface ProcessListOptions {
|
|
25
|
+
/** 过滤表达式列表,例如 `["status=active", "pri>=2"]`,多条之间按 AND 组合。 */
|
|
26
|
+
filter?: string[];
|
|
27
|
+
/** 模糊搜索关键词组,组内空格分隔为 OR,多组之间按 AND 组合。 */
|
|
28
|
+
search?: string[];
|
|
29
|
+
/** 限定搜索字段,缺省时搜索全部字段。 */
|
|
30
|
+
searchFields?: string[];
|
|
31
|
+
/** 排序表达式,多个字段以英文逗号分隔,例如 `pri:desc,id:asc`。 */
|
|
32
|
+
sort?: string;
|
|
33
|
+
/** 限制返回列表数量,在排序后、摘取前截断;不改变服务端页大小。 */
|
|
34
|
+
limit?: string;
|
|
35
|
+
/** 摘取字段路径列表。 */
|
|
36
|
+
pick?: string[];
|
|
37
|
+
}
|
|
38
|
+
/** {@link processData} 处理单条对象时的选项。 */
|
|
39
|
+
export interface ProcessSingleOptions {
|
|
40
|
+
/** 摘取字段路径列表。 */
|
|
41
|
+
pick?: string[];
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|