zentao-api 0.1.0 → 0.2.0-beta.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.
- package/LICENSE +2 -2
- package/README.md +103 -147
- package/dist/browser/zentao-api.global.js +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +1 -0
- package/dist/client/index.d.ts +37 -0
- package/dist/client/index.js +149 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -8
- package/dist/misc/browser-global.d.ts +1 -0
- package/dist/misc/browser-global.js +8 -0
- package/dist/misc/environment.d.ts +6 -0
- package/dist/misc/environment.js +30 -0
- package/dist/misc/errors.d.ts +25 -0
- package/dist/misc/errors.js +35 -0
- package/dist/misc/global-options.d.ts +5 -0
- package/dist/misc/global-options.js +9 -0
- package/dist/modules/generated.d.ts +8 -0
- package/dist/modules/generated.js +4226 -0
- package/dist/modules/registry.d.ts +22 -0
- package/dist/modules/registry.js +129 -0
- package/dist/modules/resolve.d.ts +7 -0
- package/dist/modules/resolve.js +196 -0
- package/dist/request/index.d.ts +7 -0
- package/dist/request/index.js +65 -0
- package/dist/types/index.d.ts +235 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +14 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/package.json +43 -77
- package/dist/types.d.ts +0 -70
- package/dist/utils.d.ts +0 -93
- package/dist/zentao-api.cjs.development.js +0 -3619
- package/dist/zentao-api.cjs.development.js.map +0 -1
- package/dist/zentao-api.cjs.production.min.js +0 -2
- package/dist/zentao-api.cjs.production.min.js.map +0 -1
- package/dist/zentao-api.esm.js +0 -3611
- package/dist/zentao-api.esm.js.map +0 -1
- package/dist/zentao-config.d.ts +0 -93
- package/dist/zentao-request-builder.d.ts +0 -120
- package/dist/zentao.d.ts +0 -175
- package/dist/zentao12.d.ts +0 -676
- package/src/index.ts +0 -5
- package/src/types.ts +0 -88
- package/src/utils.ts +0 -216
- package/src/zentao-config.ts +0 -150
- package/src/zentao-request-builder.ts +0 -227
- package/src/zentao.ts +0 -596
- package/src/zentao12.ts +0 -1272
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ModuleAction, ModuleDefinition } from '../types/index.js';
|
|
2
|
+
import { BUILTIN_MODULES } from './generated.js';
|
|
3
|
+
export { BUILTIN_MODULES };
|
|
4
|
+
export declare const MODULES: ModuleDefinition[];
|
|
5
|
+
export interface DefineModulesOptions {
|
|
6
|
+
/** 同名模块是否整体替换;默认合并模块定义和动作。 */
|
|
7
|
+
relace?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** 定义或扩展模块;同名模块默认合并动作,`relace` 为真时整体替换,未知模块追加。 */
|
|
10
|
+
export declare function defineModules(input: ModuleDefinition | ModuleDefinition[], options?: DefineModulesOptions): void;
|
|
11
|
+
/** 为已存在模块定义或覆盖动作;同名动作替换,未知动作追加。 */
|
|
12
|
+
export declare function defineModuleActions(moduleName: string, input: ModuleAction | ModuleAction[]): void;
|
|
13
|
+
/** 获取模块定义;模块不存在时抛出 {@link ZentaoError}。 */
|
|
14
|
+
export declare function getModule(moduleName: string): ModuleDefinition;
|
|
15
|
+
/** 获取指定模块动作;`ls` 会作为 `list` 的别名处理。 */
|
|
16
|
+
export declare function getModuleAction(moduleName: string, actionName: string): ModuleAction;
|
|
17
|
+
/** 返回当前运行时注册表中的所有模块名。 */
|
|
18
|
+
export declare function getModuleNames(): string[];
|
|
19
|
+
/** 判断模块名是否已注册,大小写不敏感。 */
|
|
20
|
+
export declare function isModuleName(moduleName: string): boolean;
|
|
21
|
+
/** @internal */
|
|
22
|
+
export declare function resetModuleDefinitions(): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { asArray } from '../utils/index.js';
|
|
3
|
+
import { BUILTIN_MODULES } from './generated.js';
|
|
4
|
+
export { BUILTIN_MODULES };
|
|
5
|
+
export const MODULES = BUILTIN_MODULES;
|
|
6
|
+
// 运行时注册表使用内置定义的浅克隆,避免用户扩展污染生成文件导出的常量。
|
|
7
|
+
let modules = cloneModules(BUILTIN_MODULES);
|
|
8
|
+
let moduleMap = buildModuleMap(modules);
|
|
9
|
+
function cloneActions(source) {
|
|
10
|
+
return source.map((action) => ({ ...action }));
|
|
11
|
+
}
|
|
12
|
+
function cloneModules(source) {
|
|
13
|
+
return source.map((module) => ({
|
|
14
|
+
...module,
|
|
15
|
+
actions: cloneActions(module.actions),
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
function findActionIndex(source, actionName) {
|
|
19
|
+
const key = actionName.toLowerCase();
|
|
20
|
+
return source.findIndex((action) => String(action.name).toLowerCase() === key);
|
|
21
|
+
}
|
|
22
|
+
function mergeActions(base, extension) {
|
|
23
|
+
const next = cloneActions(base);
|
|
24
|
+
for (const action of extension) {
|
|
25
|
+
const index = findActionIndex(next, String(action.name));
|
|
26
|
+
const clone = { ...action };
|
|
27
|
+
if (index >= 0) {
|
|
28
|
+
next[index] = clone;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
next.push(clone);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return next;
|
|
35
|
+
}
|
|
36
|
+
function mergeModule(base, extension) {
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
...extension,
|
|
40
|
+
actions: mergeActions(base.actions, extension.actions),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function buildModuleMap(source) {
|
|
44
|
+
return new Map(source.map((module) => [module.name.toLowerCase(), module]));
|
|
45
|
+
}
|
|
46
|
+
function rebuildMap() {
|
|
47
|
+
moduleMap = buildModuleMap(modules);
|
|
48
|
+
}
|
|
49
|
+
function validateModule(module) {
|
|
50
|
+
if (!module || typeof module.name !== 'string' || !Array.isArray(module.actions)) {
|
|
51
|
+
throw new ZentaoError('E_INVALID_MODULE_DEFINITION');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function validateAction(action) {
|
|
55
|
+
if (!action || typeof action.name !== 'string' || typeof action.path !== 'string' || typeof action.method !== 'string') {
|
|
56
|
+
throw new ZentaoError('E_INVALID_ACTION_DEFINITION');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** 定义或扩展模块;同名模块默认合并动作,`relace` 为真时整体替换,未知模块追加。 */
|
|
60
|
+
export function defineModules(input, options = {}) {
|
|
61
|
+
for (const module of asArray(input)) {
|
|
62
|
+
validateModule(module);
|
|
63
|
+
const key = module.name.toLowerCase();
|
|
64
|
+
const index = modules.findIndex((item) => item.name.toLowerCase() === key);
|
|
65
|
+
const next = { ...module, actions: cloneActions(module.actions) };
|
|
66
|
+
if (index >= 0) {
|
|
67
|
+
modules[index] = options.relace ? next : mergeModule(modules[index], module);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
modules.push(next);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
rebuildMap();
|
|
74
|
+
}
|
|
75
|
+
/** 为已存在模块定义或覆盖动作;同名动作替换,未知动作追加。 */
|
|
76
|
+
export function defineModuleActions(moduleName, input) {
|
|
77
|
+
const module = moduleMap.get(moduleName.toLowerCase());
|
|
78
|
+
if (!module) {
|
|
79
|
+
throw new ZentaoError('E_INVALID_MODULE', { module: moduleName });
|
|
80
|
+
}
|
|
81
|
+
for (const action of asArray(input)) {
|
|
82
|
+
validateAction(action);
|
|
83
|
+
const index = findActionIndex(module.actions, String(action.name));
|
|
84
|
+
const next = { ...action };
|
|
85
|
+
// 同名动作替换,未知动作追加;不做深度合并,避免 schema/数组字段出现隐式规则。
|
|
86
|
+
if (index >= 0) {
|
|
87
|
+
module.actions[index] = next;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
module.actions.push(next);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** 获取模块定义;模块不存在时抛出 {@link ZentaoError}。 */
|
|
95
|
+
export function getModule(moduleName) {
|
|
96
|
+
const module = moduleMap.get(moduleName.toLowerCase());
|
|
97
|
+
if (!module) {
|
|
98
|
+
throw new ZentaoError('E_INVALID_MODULE', { module: moduleName });
|
|
99
|
+
}
|
|
100
|
+
return module;
|
|
101
|
+
}
|
|
102
|
+
/** 获取指定模块动作;`ls` 会作为 `list` 的别名处理。 */
|
|
103
|
+
export function getModuleAction(moduleName, actionName) {
|
|
104
|
+
const module = getModule(moduleName);
|
|
105
|
+
const normalized = actionName === 'ls' ? 'list' : actionName;
|
|
106
|
+
const direct = module.actions.find((action) => String(action.name).toLowerCase() === normalized.toLowerCase());
|
|
107
|
+
if (direct)
|
|
108
|
+
return direct;
|
|
109
|
+
const crud = new Set(['list', 'get', 'create', 'update', 'delete']);
|
|
110
|
+
if (!crud.has(normalized)) {
|
|
111
|
+
const custom = module.actions.find((action) => action.type === 'action' && String(action.name).toLowerCase() === normalized.toLowerCase());
|
|
112
|
+
if (custom)
|
|
113
|
+
return custom;
|
|
114
|
+
}
|
|
115
|
+
throw new ZentaoError('E_INVALID_ACTION', { module: moduleName, action: actionName });
|
|
116
|
+
}
|
|
117
|
+
/** 返回当前运行时注册表中的所有模块名。 */
|
|
118
|
+
export function getModuleNames() {
|
|
119
|
+
return modules.map((module) => module.name);
|
|
120
|
+
}
|
|
121
|
+
/** 判断模块名是否已注册,大小写不敏感。 */
|
|
122
|
+
export function isModuleName(moduleName) {
|
|
123
|
+
return moduleMap.has(moduleName.toLowerCase());
|
|
124
|
+
}
|
|
125
|
+
/** @internal */
|
|
126
|
+
export function resetModuleDefinitions() {
|
|
127
|
+
modules = cloneModules(BUILTIN_MODULES);
|
|
128
|
+
rebuildMap();
|
|
129
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ListPagerInfo, ModuleAction, ModuleDefinition, ResolvedModuleCommand } from '../types/index.js';
|
|
2
|
+
/** 将模块名、动作名和调用参数解析为实际 API 请求路径、查询参数和请求体。 */
|
|
3
|
+
export declare function resolveModuleCommand(module: ModuleDefinition, actionName: string, params?: Record<string, unknown>): ResolvedModuleCommand;
|
|
4
|
+
/** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
|
|
5
|
+
export declare function extractResult(action: ModuleAction, response: Record<string, unknown>): unknown;
|
|
6
|
+
/** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
|
|
7
|
+
export declare function extractPager(action: ModuleAction, response: Record<string, unknown>): ListPagerInfo | undefined;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { getNestedValue } from '../utils/index.js';
|
|
3
|
+
import { getModuleAction } from './registry.js';
|
|
4
|
+
const SCOPE_MAP = {
|
|
5
|
+
product: 'products',
|
|
6
|
+
project: 'projects',
|
|
7
|
+
execution: 'executions',
|
|
8
|
+
};
|
|
9
|
+
const SCOPE_KEY_ORDER = ['execution', 'project', 'product'];
|
|
10
|
+
/** 从调用参数中推断作用域列表路径,优先级为执行 > 项目 > 产品。 */
|
|
11
|
+
function pickScope(params) {
|
|
12
|
+
for (const key of SCOPE_KEY_ORDER) {
|
|
13
|
+
const value = params[key] ?? params[`${key}ID`];
|
|
14
|
+
if (value === undefined || value === null || value === '')
|
|
15
|
+
continue;
|
|
16
|
+
const numberValue = Number(value);
|
|
17
|
+
if (!Number.isNaN(numberValue)) {
|
|
18
|
+
return { scope: SCOPE_MAP[key], scopeID: numberValue };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** 替换动作路径模板中的 `{param}` 占位符。 */
|
|
24
|
+
function resolvePath(action, values) {
|
|
25
|
+
return action.path.replace(/\{(\w+)\}/g, (_, key) => {
|
|
26
|
+
const value = values[key];
|
|
27
|
+
if (value === undefined || value === '') {
|
|
28
|
+
throw new ZentaoError('E_MISSING_PARAM', { param: key });
|
|
29
|
+
}
|
|
30
|
+
return String(value);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** 支持用户通过 `params.data` 传入 JSON 字符串或对象作为请求体基础值。 */
|
|
34
|
+
function parseData(value) {
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(value);
|
|
40
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
function isPlainObject(value) {
|
|
49
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
/** 按 OpenAPI schema 的基础类型对参数做轻量转换。 */
|
|
52
|
+
function coerceValue(value, type) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (type === 'number' || type === 'integer') {
|
|
56
|
+
const numberValue = Number(value);
|
|
57
|
+
return Number.isNaN(numberValue) ? value : numberValue;
|
|
58
|
+
}
|
|
59
|
+
if (type === 'boolean') {
|
|
60
|
+
if (value === 'true')
|
|
61
|
+
return true;
|
|
62
|
+
if (value === 'false')
|
|
63
|
+
return false;
|
|
64
|
+
return Boolean(value);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
/** 将模块名、动作名和调用参数解析为实际 API 请求路径、查询参数和请求体。 */
|
|
69
|
+
export function resolveModuleCommand(module, actionName, params = {}) {
|
|
70
|
+
const action = getModuleAction(module.name, actionName);
|
|
71
|
+
const pathValues = {};
|
|
72
|
+
const pathParamNames = Object.keys(action.pathParams ?? {});
|
|
73
|
+
// 生成定义中的 scope 列表接口会统一成 /{scope}/{scopeID}/xxx。
|
|
74
|
+
if (pathParamNames.includes('scope')) {
|
|
75
|
+
const scope = pickScope(params);
|
|
76
|
+
if (!scope)
|
|
77
|
+
throw new ZentaoError('E_MISSING_PARAM', { param: 'product/project/execution' });
|
|
78
|
+
pathValues.scope = scope.scope;
|
|
79
|
+
pathValues.scopeID = scope.scopeID;
|
|
80
|
+
}
|
|
81
|
+
const idParamName = pathParamNames.find((key) => key.endsWith('ID') && key !== 'scopeID');
|
|
82
|
+
const idValue = params.id ?? params[`${module.name}ID`] ?? (idParamName ? params[idParamName] : undefined);
|
|
83
|
+
const id = idValue === undefined ? undefined : Number(idValue);
|
|
84
|
+
if (idParamName && id !== undefined && !Number.isNaN(id)) {
|
|
85
|
+
pathValues[idParamName] = id;
|
|
86
|
+
}
|
|
87
|
+
// pathParams 中未显式传值的参数,可从定义里的默认值或第一个可选项补齐。
|
|
88
|
+
for (const key of pathParamNames) {
|
|
89
|
+
if (key === 'scope' || key === 'scopeID' || pathValues[key] !== undefined)
|
|
90
|
+
continue;
|
|
91
|
+
const definition = action.pathParams?.[key];
|
|
92
|
+
const value = params[key];
|
|
93
|
+
if (value !== undefined) {
|
|
94
|
+
pathValues[key] = value;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (typeof definition === 'object') {
|
|
98
|
+
if (definition.defaultValue !== undefined) {
|
|
99
|
+
pathValues[key] = definition.defaultValue;
|
|
100
|
+
}
|
|
101
|
+
else if (definition.options?.[0]?.value !== undefined) {
|
|
102
|
+
pathValues[key] = definition.options[0].value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (pathValues[key] === undefined) {
|
|
106
|
+
throw new ZentaoError('E_MISSING_PARAM', { param: key });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 查询参数只从 action.params 中声明过的字段生成,避免把 body 字段误放到 URL 上。
|
|
110
|
+
const query = {};
|
|
111
|
+
for (const param of action.params ?? []) {
|
|
112
|
+
let value = params[param.name];
|
|
113
|
+
if (value === undefined && param.name === 'pageID') {
|
|
114
|
+
value = params.page;
|
|
115
|
+
}
|
|
116
|
+
if (value === undefined) {
|
|
117
|
+
value = param.defaultValue ?? param.options?.[0]?.value;
|
|
118
|
+
}
|
|
119
|
+
if (value === undefined && param.required) {
|
|
120
|
+
throw new ZentaoError('E_MISSING_PARAM', { param: param.name });
|
|
121
|
+
}
|
|
122
|
+
if (value !== undefined) {
|
|
123
|
+
query[param.name] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let data = parseData(params.data);
|
|
127
|
+
if (action.requestBody?.schema?.type === 'object') {
|
|
128
|
+
data = data ? { ...data } : {};
|
|
129
|
+
const schema = action.requestBody.schema;
|
|
130
|
+
const required = new Set(schema.required ?? []);
|
|
131
|
+
for (const [key, property] of Object.entries(schema.properties ?? {})) {
|
|
132
|
+
// body 字段优先级:params.data 中的字段 > 平铺 params 字段 > schema 默认值。
|
|
133
|
+
const hasDataValue = Object.prototype.hasOwnProperty.call(data, key);
|
|
134
|
+
let value = data[key] ?? params[key] ?? property.defaultValue;
|
|
135
|
+
if (value === undefined && (property.required || required.has(key))) {
|
|
136
|
+
throw new ZentaoError('E_MISSING_PARAM', { param: key });
|
|
137
|
+
}
|
|
138
|
+
value = coerceValue(value, property.type);
|
|
139
|
+
if (property.type === 'array' && value !== undefined && !Array.isArray(value)) {
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
value = value.split(',');
|
|
142
|
+
}
|
|
143
|
+
else if (!hasDataValue || !isPlainObject(value)) {
|
|
144
|
+
value = [value];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (value !== undefined) {
|
|
148
|
+
data[key] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
module: module.name,
|
|
154
|
+
action,
|
|
155
|
+
params,
|
|
156
|
+
path: resolvePath(action, pathValues),
|
|
157
|
+
query,
|
|
158
|
+
data,
|
|
159
|
+
id: id === undefined || Number.isNaN(id) ? undefined : id,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** 根据动作定义中的 resultGetter,从原始响应里提取业务数据。 */
|
|
163
|
+
export function extractResult(action, response) {
|
|
164
|
+
const getter = action.resultGetter;
|
|
165
|
+
if (!getter)
|
|
166
|
+
return response.data ?? response;
|
|
167
|
+
if (typeof getter === 'function')
|
|
168
|
+
return getter(response, {});
|
|
169
|
+
if (typeof getter === 'string')
|
|
170
|
+
return getNestedValue(response, getter);
|
|
171
|
+
const result = {};
|
|
172
|
+
for (const [targetKey, sourceKey] of Object.entries(getter)) {
|
|
173
|
+
result[targetKey] = response[sourceKey];
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
/** 根据动作定义中的 pagerGetter,从原始响应里提取分页信息。 */
|
|
178
|
+
export function extractPager(action, response) {
|
|
179
|
+
const getter = action.pagerGetter;
|
|
180
|
+
if (!getter)
|
|
181
|
+
return response.pager;
|
|
182
|
+
if (typeof getter === 'function')
|
|
183
|
+
return getter(response, {});
|
|
184
|
+
if (typeof getter === 'string')
|
|
185
|
+
return getNestedValue(response, getter);
|
|
186
|
+
const page = response[getter.pageID];
|
|
187
|
+
const recPerPage = response[getter.recPerPage];
|
|
188
|
+
const recTotal = response[getter.recTotal];
|
|
189
|
+
if (page === undefined || recPerPage === undefined || recTotal === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
return {
|
|
192
|
+
pageID: Number(page),
|
|
193
|
+
recPerPage: Number(recPerPage),
|
|
194
|
+
recTotal: Number(recTotal),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RequestOptions, ResponseData } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 按模块动作名请求禅道 API。
|
|
4
|
+
*
|
|
5
|
+
* 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
|
|
6
|
+
*/
|
|
7
|
+
export declare function request(name: `${string}/${string}`, params?: Record<string, unknown>, options?: RequestOptions): Promise<ResponseData>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { getGlobalOptions } from '../misc/global-options.js';
|
|
3
|
+
import { getModule } from '../modules/registry.js';
|
|
4
|
+
import { extractPager, extractResult, resolveModuleCommand } from '../modules/resolve.js';
|
|
5
|
+
/** 将 `moduleName/methodName` 形式的请求名拆成模块名和动作名。 */
|
|
6
|
+
function splitRequestName(name) {
|
|
7
|
+
const index = name.indexOf('/');
|
|
8
|
+
if (index <= 0 || index === name.length - 1) {
|
|
9
|
+
throw new ZentaoError('E_INVALID_REQUEST_NAME');
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
moduleName: name.slice(0, index),
|
|
13
|
+
actionName: name.slice(index + 1),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** 将禅道原始响应归一化为稳定的 ResponseData 结构。 */
|
|
17
|
+
function normalizeResponse(command, raw, limit) {
|
|
18
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
19
|
+
return { status: 'success', data: raw };
|
|
20
|
+
}
|
|
21
|
+
const record = raw;
|
|
22
|
+
const status = record.status === 'fail' ? 'fail' : 'success';
|
|
23
|
+
let data = extractResult(command.action, record);
|
|
24
|
+
const numericLimit = limit === undefined ? undefined : Number(limit);
|
|
25
|
+
if (Array.isArray(data) && numericLimit !== undefined && !Number.isNaN(numericLimit)) {
|
|
26
|
+
data = data.slice(0, numericLimit);
|
|
27
|
+
}
|
|
28
|
+
const pager = extractPager(command.action, record);
|
|
29
|
+
return {
|
|
30
|
+
status,
|
|
31
|
+
message: typeof record.message === 'string' ? record.message : undefined,
|
|
32
|
+
data,
|
|
33
|
+
pager: pager ? {
|
|
34
|
+
total: Number(pager.recTotal),
|
|
35
|
+
page: Number(pager.pageID),
|
|
36
|
+
recPerPage: Number(pager.recPerPage),
|
|
37
|
+
} : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 按模块动作名请求禅道 API。
|
|
42
|
+
*
|
|
43
|
+
* 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
|
|
44
|
+
*/
|
|
45
|
+
export async function request(name, params = {}, options = {}) {
|
|
46
|
+
const globals = getGlobalOptions();
|
|
47
|
+
const client = options.client ?? globals.client;
|
|
48
|
+
if (!client) {
|
|
49
|
+
throw new ZentaoError('E_NO_GLOBAL_CLIENT');
|
|
50
|
+
}
|
|
51
|
+
const { moduleName, actionName } = splitRequestName(name);
|
|
52
|
+
const module = getModule(moduleName);
|
|
53
|
+
// recPerPage 是最常用的列表参数,允许在全局或本次调用中统一覆盖。
|
|
54
|
+
const recPerPage = params.recPerPage ?? options.recPerPage ?? globals.recPerPage;
|
|
55
|
+
const mergedParams = recPerPage === undefined ? params : { ...params, recPerPage };
|
|
56
|
+
const command = resolveModuleCommand(module, actionName, mergedParams);
|
|
57
|
+
const raw = await client.request(command.path, {
|
|
58
|
+
method: String(command.action.method).toUpperCase(),
|
|
59
|
+
query: command.query,
|
|
60
|
+
body: command.data,
|
|
61
|
+
timeout: options.timeout ?? globals.timeout,
|
|
62
|
+
insecure: options.insecure ?? globals.insecure,
|
|
63
|
+
});
|
|
64
|
+
return normalizeResponse(command, raw, options.limit ?? globals.limit);
|
|
65
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { ZentaoClient } from '../client/index.js';
|
|
2
|
+
/** 创建 {@link ZentaoClient} 时使用的配置。 */
|
|
3
|
+
export interface ZentaoClientOptions {
|
|
4
|
+
/** 禅道站点根地址,例如 `https://zentao.example.com`;SDK 会自动拼接 `/api.php/v2`。 */
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/** 禅道 API Token;未提供时可稍后通过 {@link ZentaoClient.login} 获取并写入实例。 */
|
|
7
|
+
token?: string;
|
|
8
|
+
/** 默认请求超时时间,单位毫秒。 */
|
|
9
|
+
timeout?: number;
|
|
10
|
+
/** 是否跳过 TLS 证书验证;仅 Node.js 运行时支持,浏览器中会抛错。 */
|
|
11
|
+
insecure?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** SDK 进程级全局默认选项,供高阶 {@link request} 调用复用。 */
|
|
14
|
+
export interface GlobalOptions {
|
|
15
|
+
/** 默认客户端;通常由 `ZentaoClient.init()` 设置。 */
|
|
16
|
+
client?: ZentaoClient;
|
|
17
|
+
/** 默认每页记录数,会映射到模块动作的 `recPerPage` 参数。 */
|
|
18
|
+
recPerPage?: string;
|
|
19
|
+
/** 默认限制返回列表数量,只影响 SDK 归一化后的 `data`。 */
|
|
20
|
+
limit?: string;
|
|
21
|
+
/** 默认请求超时时间,优先级低于单次请求选项。 */
|
|
22
|
+
timeout?: number;
|
|
23
|
+
/** 默认 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
24
|
+
insecure?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** SDK 支持的 HTTP 方法。 */
|
|
27
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
28
|
+
/** `ZentaoClient.request()` 的单次请求选项。 */
|
|
29
|
+
export interface ClientRequestOptions {
|
|
30
|
+
/** HTTP 方法,默认 `GET`。 */
|
|
31
|
+
method?: HttpMethod;
|
|
32
|
+
/** JSON 请求体;`GET` 请求会忽略该字段。 */
|
|
33
|
+
body?: Record<string, unknown>;
|
|
34
|
+
/** URL 查询参数;`undefined` 值会被跳过。 */
|
|
35
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
36
|
+
/** 单次请求超时时间,优先级高于全局和客户端默认值。 */
|
|
37
|
+
timeout?: number;
|
|
38
|
+
/** 单次请求 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
39
|
+
insecure?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** 高阶 `request("moduleName/methodName")` 的单次调用选项。 */
|
|
42
|
+
export interface RequestOptions {
|
|
43
|
+
/** 本次调用使用的客户端;优先级高于全局客户端。 */
|
|
44
|
+
client?: ZentaoClient;
|
|
45
|
+
/** 本次调用使用的每页记录数,优先级高于全局 `recPerPage`。 */
|
|
46
|
+
recPerPage?: string;
|
|
47
|
+
/** 本次调用限制返回列表数量,优先级高于全局 `limit`。 */
|
|
48
|
+
limit?: string;
|
|
49
|
+
/** 本次调用超时时间。 */
|
|
50
|
+
timeout?: number;
|
|
51
|
+
/** 本次调用 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
52
|
+
insecure?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** 高阶 `request()` 归一化后的返回数据。 */
|
|
55
|
+
export interface ResponseData {
|
|
56
|
+
/** 禅道服务端状态;非标准响应会按成功响应包装到 `data`。 */
|
|
57
|
+
status: 'success' | 'fail';
|
|
58
|
+
/** 禅道服务端返回的消息。 */
|
|
59
|
+
message?: string;
|
|
60
|
+
/** 根据模块动作 `resultGetter` 提取后的业务数据。 */
|
|
61
|
+
data?: any;
|
|
62
|
+
/** 统一分页信息。 */
|
|
63
|
+
pager?: {
|
|
64
|
+
/** 总记录数。 */
|
|
65
|
+
total: number;
|
|
66
|
+
/** 当前页码。 */
|
|
67
|
+
page: number;
|
|
68
|
+
/** 每页记录数。 */
|
|
69
|
+
recPerPage: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** 禅道 API 原始分页结构。 */
|
|
73
|
+
export interface Pager {
|
|
74
|
+
/** 总记录数。 */
|
|
75
|
+
recTotal: number;
|
|
76
|
+
/** 每页记录数。 */
|
|
77
|
+
recPerPage: number;
|
|
78
|
+
/** 总页数,部分接口不返回。 */
|
|
79
|
+
pageTotal?: number;
|
|
80
|
+
/** 当前页码。 */
|
|
81
|
+
pageID: number;
|
|
82
|
+
}
|
|
83
|
+
/** 禅道 API 通用响应结构,允许携带任意业务字段。 */
|
|
84
|
+
export interface ApiResponse {
|
|
85
|
+
/** 服务端返回状态。 */
|
|
86
|
+
status: 'success' | 'fail';
|
|
87
|
+
/** 服务端消息,可能是字符串、对象或数组。 */
|
|
88
|
+
message?: unknown;
|
|
89
|
+
/** 其他业务字段。 */
|
|
90
|
+
[key: string]: unknown;
|
|
91
|
+
}
|
|
92
|
+
/** 禅道 API 列表响应结构。 */
|
|
93
|
+
export interface ApiListResponse extends ApiResponse {
|
|
94
|
+
/** 原始分页信息。 */
|
|
95
|
+
pager?: Pager;
|
|
96
|
+
}
|
|
97
|
+
/** 登录接口响应结构。 */
|
|
98
|
+
export interface LoginResponse extends ApiResponse {
|
|
99
|
+
/** 登录成功后返回的 API Token。 */
|
|
100
|
+
token?: string;
|
|
101
|
+
}
|
|
102
|
+
/** 禅道 `?mode=getconfig` 返回的服务端配置。 */
|
|
103
|
+
export interface ServerConfig {
|
|
104
|
+
version: string;
|
|
105
|
+
systemMode: string;
|
|
106
|
+
sprintConcept: string;
|
|
107
|
+
requestType: string;
|
|
108
|
+
requestFix: string;
|
|
109
|
+
moduleVar: string;
|
|
110
|
+
methodVar: string;
|
|
111
|
+
viewVar: string;
|
|
112
|
+
sessionVar: string;
|
|
113
|
+
}
|
|
114
|
+
/** 模块动作类型:基础 CRUD 或自定义动作。 */
|
|
115
|
+
export type ModuleActionType = 'list' | 'get' | 'create' | 'update' | 'delete' | 'action';
|
|
116
|
+
/** 模块动作使用的 HTTP 方法;兼容生成定义中的小写方法。 */
|
|
117
|
+
export type ModuleActionMethod = HttpMethod | Lowercase<HttpMethod>;
|
|
118
|
+
/** 模块动作名称,允许除基础动作外的自定义名称。 */
|
|
119
|
+
export type ModuleActionName = ModuleActionType | (string & {});
|
|
120
|
+
/** 模块动作参数可选项。 */
|
|
121
|
+
export type ModuleActionParamOption = {
|
|
122
|
+
value: unknown;
|
|
123
|
+
label: string;
|
|
124
|
+
};
|
|
125
|
+
/** 模块动作的查询参数定义。 */
|
|
126
|
+
export interface ModuleActionParam {
|
|
127
|
+
/** 参数名称。 */
|
|
128
|
+
name: string;
|
|
129
|
+
/** 参数说明。 */
|
|
130
|
+
description?: string;
|
|
131
|
+
/** 是否必填。 */
|
|
132
|
+
required?: boolean;
|
|
133
|
+
/** 未显式传入时使用的默认值。 */
|
|
134
|
+
defaultValue?: unknown;
|
|
135
|
+
/** 参数值类型,用于基础类型转换。 */
|
|
136
|
+
type?: 'string' | 'number' | 'boolean';
|
|
137
|
+
/** 参数可选值。 */
|
|
138
|
+
options?: ModuleActionParamOption[];
|
|
139
|
+
}
|
|
140
|
+
/** 模块动作结果形态。 */
|
|
141
|
+
export type ModuleActionResultType = 'text' | 'object' | 'list';
|
|
142
|
+
/** 列表分页信息别名。 */
|
|
143
|
+
export type ListPagerInfo = Pager;
|
|
144
|
+
/** 模块动作请求体定义。 */
|
|
145
|
+
export interface ModuleActionRequestBody {
|
|
146
|
+
/** 请求体类型。 */
|
|
147
|
+
type?: 'object' | 'string';
|
|
148
|
+
/** 请求体是否必填。 */
|
|
149
|
+
required?: boolean;
|
|
150
|
+
/** OpenAPI 风格 schema,用于从 params 组装 body。 */
|
|
151
|
+
schema: Record<string, unknown>;
|
|
152
|
+
/** 请求体示例。 */
|
|
153
|
+
example?: unknown;
|
|
154
|
+
}
|
|
155
|
+
/** 模块动作响应定义。 */
|
|
156
|
+
export interface ModuleActionResponse {
|
|
157
|
+
/** 响应说明。 */
|
|
158
|
+
description?: string;
|
|
159
|
+
/** 响应 schema。 */
|
|
160
|
+
schema: Record<string, unknown>;
|
|
161
|
+
/** 响应示例。 */
|
|
162
|
+
example?: unknown;
|
|
163
|
+
}
|
|
164
|
+
/** 模块动作渲染目标类型;保留给 CLI 等上层应用使用。 */
|
|
165
|
+
export type ModuleActionResultRenderType = 'markdown' | 'json' | 'raw';
|
|
166
|
+
/** 模块动作自定义渲染函数类型;SDK 本身不直接渲染终端输出。 */
|
|
167
|
+
export type ModuleActionResultRender = (result: unknown, type: ModuleActionResultRenderType, action: ModuleAction) => string;
|
|
168
|
+
/** 从原始响应中提取分页字段时使用的字段映射。 */
|
|
169
|
+
export interface ModuleActionPagerGetterMap {
|
|
170
|
+
/** 当前页码字段名。 */
|
|
171
|
+
pageID: string;
|
|
172
|
+
/** 每页记录数字段名。 */
|
|
173
|
+
recPerPage: string;
|
|
174
|
+
/** 总记录数字段名。 */
|
|
175
|
+
recTotal: string;
|
|
176
|
+
}
|
|
177
|
+
/** 禅道模块中的单个 API 动作定义。 */
|
|
178
|
+
export interface ModuleAction {
|
|
179
|
+
/** 动作名称,例如 `list`、`get`、`close`。 */
|
|
180
|
+
name: ModuleActionName;
|
|
181
|
+
/** 动作类型,决定高阶 request 的路径/参数解析策略。 */
|
|
182
|
+
type: ModuleActionType;
|
|
183
|
+
/** 面向用户展示的动作名称。 */
|
|
184
|
+
display?: string;
|
|
185
|
+
/** 动作说明。 */
|
|
186
|
+
description?: string;
|
|
187
|
+
/** HTTP 方法。 */
|
|
188
|
+
method: ModuleActionMethod;
|
|
189
|
+
/** API 路径模板,可包含 `{productID}` 等路径参数。 */
|
|
190
|
+
path: string;
|
|
191
|
+
/** 路径参数定义;字符串为说明,对象可携带默认值和可选项。 */
|
|
192
|
+
pathParams?: Record<string, string | Omit<ModuleActionParam, 'name'>>;
|
|
193
|
+
/** 查询参数定义。 */
|
|
194
|
+
params?: ModuleActionParam[];
|
|
195
|
+
/** 请求体定义。 */
|
|
196
|
+
requestBody?: ModuleActionRequestBody;
|
|
197
|
+
/** 结果形态。 */
|
|
198
|
+
resultType: ModuleActionResultType;
|
|
199
|
+
/** 从原始响应中提取分页信息的位置或函数。 */
|
|
200
|
+
pagerGetter?: string | ModuleActionPagerGetterMap | ((data: unknown, params: Record<string, unknown>) => ListPagerInfo);
|
|
201
|
+
/** 从原始响应中提取业务数据的位置或函数。 */
|
|
202
|
+
resultGetter?: string | Record<string, string> | ((data: unknown, params: Record<string, unknown>) => unknown);
|
|
203
|
+
/** 供上层应用使用的渲染配置。 */
|
|
204
|
+
render?: string | ModuleActionResultRender | Record<ModuleActionResultRenderType, ModuleActionResultRender>;
|
|
205
|
+
}
|
|
206
|
+
/** 内置模块名称,同时允许用户扩展自定义模块名。 */
|
|
207
|
+
export type ModuleName = 'user' | 'program' | 'product' | 'project' | 'execution' | 'productplan' | 'story' | 'epic' | 'requirement' | 'bug' | 'testcase' | 'task' | 'feedback' | 'ticket' | 'system' | 'build' | 'testtask' | 'release' | 'file' | (string & {});
|
|
208
|
+
/** 禅道模块定义,由多个动作组成。 */
|
|
209
|
+
export interface ModuleDefinition {
|
|
210
|
+
/** 模块名称,例如 `product`、`bug`。 */
|
|
211
|
+
name: ModuleName;
|
|
212
|
+
/** 面向用户展示的模块名称。 */
|
|
213
|
+
display?: string;
|
|
214
|
+
/** 模块说明。 */
|
|
215
|
+
description?: string;
|
|
216
|
+
/** 模块支持的动作集合。 */
|
|
217
|
+
actions: ModuleAction[];
|
|
218
|
+
}
|
|
219
|
+
/** 将模块动作和参数解析后的可执行请求描述。 */
|
|
220
|
+
export interface ResolvedModuleCommand {
|
|
221
|
+
/** 模块名称。 */
|
|
222
|
+
module: string;
|
|
223
|
+
/** 匹配到的动作定义。 */
|
|
224
|
+
action: ModuleAction;
|
|
225
|
+
/** 原始调用参数。 */
|
|
226
|
+
params: Record<string, unknown>;
|
|
227
|
+
/** 已替换路径参数后的 API 路径。 */
|
|
228
|
+
path: string;
|
|
229
|
+
/** 已组装的查询参数。 */
|
|
230
|
+
query?: Record<string, string | number>;
|
|
231
|
+
/** 已组装的请求体。 */
|
|
232
|
+
data?: Record<string, unknown>;
|
|
233
|
+
/** 从 `id` 或 `{module}ID` 推断出的对象 ID。 */
|
|
234
|
+
id?: number;
|
|
235
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|