zentao-api 0.6.0 → 0.6.2
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 +16 -0
- package/dist/browser/zentao-api.global.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/misc/errors.d.ts +5 -0
- package/dist/misc/errors.js +5 -0
- package/dist/modules/export.d.ts +3 -1
- package/dist/modules/export.js +1 -0
- package/dist/modules/generated.d.ts +105 -2
- package/dist/modules/generated.js +85 -25
- package/dist/modules/override.js +10 -0
- package/dist/modules/registry.d.ts +102 -2
- package/dist/request/index.d.ts +3 -3
- package/dist/request/index.js +6 -1
- package/dist/request/prepare-body.d.ts +12 -0
- package/dist/request/prepare-body.js +221 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/module.d.ts +6 -0
- package/dist/types/options.d.ts +2 -0
- package/dist/types/upload.d.ts +38 -0
- package/dist/types/upload.js +1 -0
- package/dist/version.js +2 -2
- package/package.json +1 -1
|
@@ -3403,6 +3403,9 @@ export declare const MODULES: ({
|
|
|
3403
3403
|
name?: undefined;
|
|
3404
3404
|
parent?: undefined;
|
|
3405
3405
|
comment?: undefined;
|
|
3406
|
+
deadline?: undefined;
|
|
3407
|
+
status?: undefined;
|
|
3408
|
+
mailto?: undefined;
|
|
3406
3409
|
resolution?: undefined;
|
|
3407
3410
|
resolvedDate?: undefined;
|
|
3408
3411
|
resolvedBuild?: undefined;
|
|
@@ -3504,6 +3507,9 @@ export declare const MODULES: ({
|
|
|
3504
3507
|
name?: undefined;
|
|
3505
3508
|
parent?: undefined;
|
|
3506
3509
|
comment?: undefined;
|
|
3510
|
+
deadline?: undefined;
|
|
3511
|
+
status?: undefined;
|
|
3512
|
+
mailto?: undefined;
|
|
3507
3513
|
resolution?: undefined;
|
|
3508
3514
|
resolvedDate?: undefined;
|
|
3509
3515
|
resolvedBuild?: undefined;
|
|
@@ -3555,6 +3561,9 @@ export declare const MODULES: ({
|
|
|
3555
3561
|
story?: undefined;
|
|
3556
3562
|
assignedTo?: undefined;
|
|
3557
3563
|
comment?: undefined;
|
|
3564
|
+
deadline?: undefined;
|
|
3565
|
+
status?: undefined;
|
|
3566
|
+
mailto?: undefined;
|
|
3558
3567
|
resolution?: undefined;
|
|
3559
3568
|
resolvedDate?: undefined;
|
|
3560
3569
|
resolvedBuild?: undefined;
|
|
@@ -3647,6 +3656,9 @@ export declare const MODULES: ({
|
|
|
3647
3656
|
story?: undefined;
|
|
3648
3657
|
name?: undefined;
|
|
3649
3658
|
parent?: undefined;
|
|
3659
|
+
deadline?: undefined;
|
|
3660
|
+
status?: undefined;
|
|
3661
|
+
mailto?: undefined;
|
|
3650
3662
|
resolution?: undefined;
|
|
3651
3663
|
resolvedDate?: undefined;
|
|
3652
3664
|
resolvedBuild?: undefined;
|
|
@@ -3694,6 +3706,81 @@ export declare const MODULES: ({
|
|
|
3694
3706
|
assignedTo?: undefined;
|
|
3695
3707
|
name?: undefined;
|
|
3696
3708
|
parent?: undefined;
|
|
3709
|
+
deadline?: undefined;
|
|
3710
|
+
status?: undefined;
|
|
3711
|
+
mailto?: undefined;
|
|
3712
|
+
resolution?: undefined;
|
|
3713
|
+
resolvedDate?: undefined;
|
|
3714
|
+
resolvedBuild?: undefined;
|
|
3715
|
+
};
|
|
3716
|
+
required?: undefined;
|
|
3717
|
+
};
|
|
3718
|
+
};
|
|
3719
|
+
pagerGetter?: undefined;
|
|
3720
|
+
resultGetter?: undefined;
|
|
3721
|
+
params?: undefined;
|
|
3722
|
+
} | {
|
|
3723
|
+
name: "confirm";
|
|
3724
|
+
display: string;
|
|
3725
|
+
type: "action";
|
|
3726
|
+
method: "put";
|
|
3727
|
+
path: string;
|
|
3728
|
+
resultType: "text";
|
|
3729
|
+
pathParams: {
|
|
3730
|
+
bugID: string;
|
|
3731
|
+
scope?: undefined;
|
|
3732
|
+
scopeID?: undefined;
|
|
3733
|
+
productID?: undefined;
|
|
3734
|
+
moduleID?: undefined;
|
|
3735
|
+
};
|
|
3736
|
+
requestBody: {
|
|
3737
|
+
required: true;
|
|
3738
|
+
type: "object";
|
|
3739
|
+
schema: {
|
|
3740
|
+
type: string;
|
|
3741
|
+
properties: {
|
|
3742
|
+
assignedTo: {
|
|
3743
|
+
type: string;
|
|
3744
|
+
description: string;
|
|
3745
|
+
};
|
|
3746
|
+
type: {
|
|
3747
|
+
type: string;
|
|
3748
|
+
description: string;
|
|
3749
|
+
};
|
|
3750
|
+
pri: {
|
|
3751
|
+
type: string;
|
|
3752
|
+
description: string;
|
|
3753
|
+
format: string;
|
|
3754
|
+
};
|
|
3755
|
+
deadline: {
|
|
3756
|
+
type: string;
|
|
3757
|
+
description: string;
|
|
3758
|
+
};
|
|
3759
|
+
status: {
|
|
3760
|
+
type: string;
|
|
3761
|
+
description: string;
|
|
3762
|
+
};
|
|
3763
|
+
mailto: {
|
|
3764
|
+
type: string;
|
|
3765
|
+
items: {
|
|
3766
|
+
type: string;
|
|
3767
|
+
};
|
|
3768
|
+
description: string;
|
|
3769
|
+
};
|
|
3770
|
+
comment: {
|
|
3771
|
+
type: string;
|
|
3772
|
+
description: string;
|
|
3773
|
+
};
|
|
3774
|
+
productID?: undefined;
|
|
3775
|
+
title?: undefined;
|
|
3776
|
+
openedBuild?: undefined;
|
|
3777
|
+
project?: undefined;
|
|
3778
|
+
execution?: undefined;
|
|
3779
|
+
severity?: undefined;
|
|
3780
|
+
steps?: undefined;
|
|
3781
|
+
story?: undefined;
|
|
3782
|
+
name?: undefined;
|
|
3783
|
+
parent?: undefined;
|
|
3697
3784
|
resolution?: undefined;
|
|
3698
3785
|
resolvedDate?: undefined;
|
|
3699
3786
|
resolvedBuild?: undefined;
|
|
@@ -3756,6 +3843,9 @@ export declare const MODULES: ({
|
|
|
3756
3843
|
story?: undefined;
|
|
3757
3844
|
name?: undefined;
|
|
3758
3845
|
parent?: undefined;
|
|
3846
|
+
deadline?: undefined;
|
|
3847
|
+
status?: undefined;
|
|
3848
|
+
mailto?: undefined;
|
|
3759
3849
|
};
|
|
3760
3850
|
required: string[];
|
|
3761
3851
|
};
|
|
@@ -7397,6 +7487,7 @@ export declare const MODULES: ({
|
|
|
7397
7487
|
path: string;
|
|
7398
7488
|
resultType: "list";
|
|
7399
7489
|
pagerGetter: string;
|
|
7490
|
+
resultGetter: string;
|
|
7400
7491
|
pathParams: {
|
|
7401
7492
|
spaceID: string;
|
|
7402
7493
|
libID: string;
|
|
@@ -7405,7 +7496,6 @@ export declare const MODULES: ({
|
|
|
7405
7496
|
docID?: undefined;
|
|
7406
7497
|
moduleID?: undefined;
|
|
7407
7498
|
};
|
|
7408
|
-
resultGetter: string;
|
|
7409
7499
|
requestBody?: undefined;
|
|
7410
7500
|
} | {
|
|
7411
7501
|
name: "productDocs";
|
|
@@ -8452,6 +8542,11 @@ export declare const MODULES: ({
|
|
|
8452
8542
|
type: string;
|
|
8453
8543
|
description: string;
|
|
8454
8544
|
};
|
|
8545
|
+
objectID: {
|
|
8546
|
+
type: string;
|
|
8547
|
+
description: string;
|
|
8548
|
+
format: string;
|
|
8549
|
+
};
|
|
8455
8550
|
begin: {
|
|
8456
8551
|
type: string;
|
|
8457
8552
|
description: string;
|
|
@@ -8501,6 +8596,11 @@ export declare const MODULES: ({
|
|
|
8501
8596
|
type: string;
|
|
8502
8597
|
description: string;
|
|
8503
8598
|
};
|
|
8599
|
+
objectID: {
|
|
8600
|
+
type: string;
|
|
8601
|
+
description: string;
|
|
8602
|
+
format: string;
|
|
8603
|
+
};
|
|
8504
8604
|
begin: {
|
|
8505
8605
|
type: string;
|
|
8506
8606
|
description: string;
|
|
@@ -9005,6 +9105,7 @@ export declare const MODULES: ({
|
|
|
9005
9105
|
path: string;
|
|
9006
9106
|
resultType: "list";
|
|
9007
9107
|
pagerGetter: string;
|
|
9108
|
+
resultGetter: string;
|
|
9008
9109
|
params: ({
|
|
9009
9110
|
name: string;
|
|
9010
9111
|
required: false;
|
|
@@ -9033,7 +9134,6 @@ export declare const MODULES: ({
|
|
|
9033
9134
|
defaultValue?: undefined;
|
|
9034
9135
|
options?: undefined;
|
|
9035
9136
|
})[];
|
|
9036
|
-
resultGetter: string;
|
|
9037
9137
|
} | {
|
|
9038
9138
|
name: "issues";
|
|
9039
9139
|
display: string;
|
package/dist/request/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DataRecord, RequestOptions, ResponseData } from '../types/index.js';
|
|
1
|
+
import type { DataRecord, FileCreateParams, FileCreateResult, RequestOptions, ResponseData } from '../types/index.js';
|
|
2
2
|
import type { BuiltinActionMeta, BuiltinModuleName } from '../modules/generated.js';
|
|
3
3
|
type BuiltinActionName<M extends BuiltinModuleName> = Extract<keyof BuiltinActionMeta[M], string>;
|
|
4
4
|
type BuiltinListRequestName = BuiltinModuleName;
|
|
@@ -14,11 +14,11 @@ type ActionMetaOf<Name extends BuiltinRequestName> = ModuleNameOf<Name> extends
|
|
|
14
14
|
resultType: 'object';
|
|
15
15
|
} : never : never;
|
|
16
16
|
/** 根据内置请求名推导出的参数类型。字段细节以运行时 registry 为准,这里保持可扩展。 */
|
|
17
|
-
export type RequestParamsFor<Name extends BuiltinRequestName> = Record<string, unknown>;
|
|
17
|
+
export type RequestParamsFor<Name extends BuiltinRequestName> = Name extends 'file/create' ? FileCreateParams : Record<string, unknown>;
|
|
18
18
|
/** 根据内置请求名推导出的 `ResponseData.data` 类型。 */
|
|
19
19
|
export type RequestResultFor<Name extends BuiltinRequestName> = ActionMetaOf<Name> extends {
|
|
20
20
|
resultType: 'list';
|
|
21
|
-
} ? DataRecord[] : ActionMetaOf<Name> extends {
|
|
21
|
+
} ? DataRecord[] : Name extends 'file/create' ? FileCreateResult : ActionMetaOf<Name> extends {
|
|
22
22
|
resultType: 'object';
|
|
23
23
|
} ? DataRecord : unknown;
|
|
24
24
|
/**
|
package/dist/request/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getGlobalOptions } from '../misc/global-options.js';
|
|
|
3
3
|
import { getModule, getModuleAction } from '../modules/registry.js';
|
|
4
4
|
import { extractPager, extractResult, resolveActionRequest } from '../modules/resolve.js';
|
|
5
5
|
import { isRecord, processData } from '../utils/index.js';
|
|
6
|
+
import { prepareActionBody } from './prepare-body.js';
|
|
6
7
|
/** 将 `moduleName`、`moduleName/methodName` 或 `moduleName/<objectID>` 请求名拆成模块名、动作名和对象 ID。 */
|
|
7
8
|
function splitRequestName(name) {
|
|
8
9
|
const parts = name.split('/');
|
|
@@ -198,10 +199,14 @@ export async function request(name, params = {}, options = {}) {
|
|
|
198
199
|
? await autoFillUpdateParams(module, action, mergedParams, options)
|
|
199
200
|
: mergedParams;
|
|
200
201
|
const command = resolveActionRequest(module, actionName, finalParams);
|
|
202
|
+
const preparedBody = await prepareActionBody(command, {
|
|
203
|
+
maxUploadBytes: options.maxUploadBytes,
|
|
204
|
+
});
|
|
201
205
|
const raw = await client.request(command.path, {
|
|
202
206
|
method: String(command.action.method).toUpperCase(),
|
|
203
207
|
query: command.query,
|
|
204
|
-
body:
|
|
208
|
+
body: preparedBody.body,
|
|
209
|
+
bodyType: preparedBody.bodyType,
|
|
205
210
|
timeout: options.timeout ?? globals.timeout,
|
|
206
211
|
insecure: options.insecure ?? globals.insecure,
|
|
207
212
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ClientRequestBodyType, ModuleActionRequest } from '../types/index.js';
|
|
2
|
+
/** 本地路径上传的默认大小上限(50 MiB)。 */
|
|
3
|
+
export declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
4
|
+
export interface PrepareActionBodyOptions {
|
|
5
|
+
maxUploadBytes?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PreparedActionBody {
|
|
8
|
+
body: unknown;
|
|
9
|
+
bodyType?: ClientRequestBodyType;
|
|
10
|
+
}
|
|
11
|
+
/** 根据模块动作的媒体类型,把解析后的普通请求体转换为底层客户端可发送的请求体。 */
|
|
12
|
+
export declare function prepareActionBody(command: ModuleActionRequest, options?: PrepareActionBodyOptions): Promise<PreparedActionBody>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { isNodeRuntime } from '../misc/environment.js';
|
|
3
|
+
import { isRecord } from '../utils/index.js';
|
|
4
|
+
/** 本地路径上传的默认大小上限(50 MiB)。 */
|
|
5
|
+
export const DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
6
|
+
const MIME_TYPES = {
|
|
7
|
+
'.avif': 'image/avif',
|
|
8
|
+
'.csv': 'text/csv',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.htm': 'text/html',
|
|
11
|
+
'.html': 'text/html',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.json': 'application/json',
|
|
15
|
+
'.md': 'text/markdown',
|
|
16
|
+
'.pdf': 'application/pdf',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.svg': 'image/svg+xml',
|
|
19
|
+
'.txt': 'text/plain',
|
|
20
|
+
'.webp': 'image/webp',
|
|
21
|
+
'.zip': 'application/zip',
|
|
22
|
+
};
|
|
23
|
+
// 与 environment.ts 相同:通过变量形式的动态 import 避免浏览器 bundle 静态拉入 node:*。
|
|
24
|
+
function importNodeModule(specifier) {
|
|
25
|
+
return import(specifier);
|
|
26
|
+
}
|
|
27
|
+
function isBlob(value) {
|
|
28
|
+
return typeof Blob !== 'undefined' && value instanceof Blob;
|
|
29
|
+
}
|
|
30
|
+
function isFileUploadPathInput(value) {
|
|
31
|
+
return isRecord(value) && typeof value.path === 'string';
|
|
32
|
+
}
|
|
33
|
+
function isFileUploadDataInput(value) {
|
|
34
|
+
return isRecord(value) && Object.prototype.hasOwnProperty.call(value, 'data') && typeof value.filename === 'string';
|
|
35
|
+
}
|
|
36
|
+
function inferContentType(filename) {
|
|
37
|
+
const normalized = filename.toLowerCase();
|
|
38
|
+
const dotIndex = normalized.lastIndexOf('.');
|
|
39
|
+
return dotIndex >= 0 ? MIME_TYPES[normalized.slice(dotIndex)] ?? 'application/octet-stream' : 'application/octet-stream';
|
|
40
|
+
}
|
|
41
|
+
function inferExtension(contentType) {
|
|
42
|
+
const normalized = contentType.split(';', 1)[0]?.trim().toLowerCase();
|
|
43
|
+
const entry = Object.entries(MIME_TYPES).find(([, type]) => type === normalized);
|
|
44
|
+
return entry?.[0] ?? '.bin';
|
|
45
|
+
}
|
|
46
|
+
function resolveMaxUploadBytes(value) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
49
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
50
|
+
throw new ZentaoError('E_INVALID_PARAM', { param: 'maxUploadBytes', value: String(value) });
|
|
51
|
+
}
|
|
52
|
+
return Math.floor(value);
|
|
53
|
+
}
|
|
54
|
+
function assertUploadSize(size, limit) {
|
|
55
|
+
if (size > limit) {
|
|
56
|
+
throw new ZentaoError('E_UPLOAD_FILE_TOO_LARGE', { size, limit });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeFilename(value, fallback, field) {
|
|
60
|
+
const requested = value?.trim() || fallback.trim();
|
|
61
|
+
const filename = requested.split(/[\\/]/).at(-1)?.trim() ?? '';
|
|
62
|
+
if (!filename) {
|
|
63
|
+
throw new ZentaoError('E_INVALID_UPLOAD_SOURCE', { field });
|
|
64
|
+
}
|
|
65
|
+
return filename;
|
|
66
|
+
}
|
|
67
|
+
function errorCode(error) {
|
|
68
|
+
return isRecord(error) && typeof error.code === 'string' ? error.code : undefined;
|
|
69
|
+
}
|
|
70
|
+
function copyView(view) {
|
|
71
|
+
const source = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
72
|
+
const copy = new Uint8Array(source.byteLength);
|
|
73
|
+
copy.set(source);
|
|
74
|
+
return copy;
|
|
75
|
+
}
|
|
76
|
+
function blobFromData(data, contentType, field) {
|
|
77
|
+
if (isBlob(data)) {
|
|
78
|
+
return data.type === contentType ? data : new Blob([data], { type: contentType });
|
|
79
|
+
}
|
|
80
|
+
if (data instanceof ArrayBuffer) {
|
|
81
|
+
return new Blob([data], { type: contentType });
|
|
82
|
+
}
|
|
83
|
+
if (ArrayBuffer.isView(data)) {
|
|
84
|
+
return new Blob([copyView(data)], { type: contentType });
|
|
85
|
+
}
|
|
86
|
+
throw new ZentaoError('E_INVALID_UPLOAD_SOURCE', { field });
|
|
87
|
+
}
|
|
88
|
+
async function preparePathFile(source, field, maxUploadBytes) {
|
|
89
|
+
if (!isNodeRuntime()) {
|
|
90
|
+
throw new ZentaoError('E_UPLOAD_PATH_UNSUPPORTED');
|
|
91
|
+
}
|
|
92
|
+
const path = typeof source === 'string' ? source : source.path;
|
|
93
|
+
const explicitFilename = typeof source === 'string' ? undefined : source.filename;
|
|
94
|
+
const explicitContentType = typeof source === 'string' ? undefined : source.contentType;
|
|
95
|
+
const [fs, pathModule] = await Promise.all([
|
|
96
|
+
importNodeModule('node:fs/promises'),
|
|
97
|
+
importNodeModule('node:path'),
|
|
98
|
+
]);
|
|
99
|
+
let stats;
|
|
100
|
+
try {
|
|
101
|
+
stats = await fs.stat(path);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (errorCode(error) === 'ENOENT') {
|
|
105
|
+
throw new ZentaoError('E_UPLOAD_FILE_NOT_FOUND', { path }, error);
|
|
106
|
+
}
|
|
107
|
+
throw new ZentaoError('E_UPLOAD_FILE_READ_FAILED', { path }, error);
|
|
108
|
+
}
|
|
109
|
+
if (!stats.isFile()) {
|
|
110
|
+
throw new ZentaoError('E_INVALID_UPLOAD_SOURCE', { field }, { path });
|
|
111
|
+
}
|
|
112
|
+
assertUploadSize(stats.size, maxUploadBytes);
|
|
113
|
+
const filename = normalizeFilename(explicitFilename, pathModule.basename(path), field);
|
|
114
|
+
const contentType = explicitContentType?.trim() || inferContentType(filename);
|
|
115
|
+
const bun = globalThis.Bun;
|
|
116
|
+
if (bun?.file) {
|
|
117
|
+
const file = bun.file(path);
|
|
118
|
+
assertUploadSize(file.size, maxUploadBytes);
|
|
119
|
+
const shouldApplyContentType = Boolean(explicitContentType?.trim()) || !file.type;
|
|
120
|
+
return {
|
|
121
|
+
blob: shouldApplyContentType && file.type !== contentType
|
|
122
|
+
? new Blob([file], { type: contentType })
|
|
123
|
+
: file,
|
|
124
|
+
filename,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const bytes = await fs.readFile(path);
|
|
129
|
+
assertUploadSize(bytes.byteLength, maxUploadBytes);
|
|
130
|
+
return {
|
|
131
|
+
blob: new Blob([copyView(bytes)], { type: contentType }),
|
|
132
|
+
filename,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new ZentaoError('E_UPLOAD_FILE_READ_FAILED', { path }, error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function prepareFile(value, field, maxUploadBytes) {
|
|
140
|
+
if (typeof value === 'string' || isFileUploadPathInput(value)) {
|
|
141
|
+
return preparePathFile(value, field, maxUploadBytes);
|
|
142
|
+
}
|
|
143
|
+
if (isBlob(value)) {
|
|
144
|
+
assertUploadSize(value.size, maxUploadBytes);
|
|
145
|
+
const contentType = value.type || 'application/octet-stream';
|
|
146
|
+
const name = typeof value.name === 'string'
|
|
147
|
+
? value.name
|
|
148
|
+
: `${field}${inferExtension(contentType)}`;
|
|
149
|
+
const filename = normalizeFilename(name, `${field}.bin`, field);
|
|
150
|
+
return {
|
|
151
|
+
blob: value.type === contentType ? value : new Blob([value], { type: contentType }),
|
|
152
|
+
filename,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (isFileUploadDataInput(value)) {
|
|
156
|
+
const filename = normalizeFilename(value.filename, field, field);
|
|
157
|
+
const contentType = value.contentType?.trim()
|
|
158
|
+
|| (isBlob(value.data) ? value.data.type : '')
|
|
159
|
+
|| inferContentType(filename);
|
|
160
|
+
const blob = blobFromData(value.data, contentType, field);
|
|
161
|
+
assertUploadSize(blob.size, maxUploadBytes);
|
|
162
|
+
return { blob, filename };
|
|
163
|
+
}
|
|
164
|
+
throw new ZentaoError('E_INVALID_UPLOAD_SOURCE', { field });
|
|
165
|
+
}
|
|
166
|
+
function isBinarySchema(schema) {
|
|
167
|
+
if (!isRecord(schema))
|
|
168
|
+
return false;
|
|
169
|
+
if (schema.format === 'binary')
|
|
170
|
+
return true;
|
|
171
|
+
return schema.type === 'array' && isRecord(schema.items) && schema.items.format === 'binary';
|
|
172
|
+
}
|
|
173
|
+
function appendTextValue(form, field, value) {
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
return;
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
for (const item of value)
|
|
178
|
+
appendTextValue(form, field, item);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (value === null) {
|
|
182
|
+
form.append(field, '');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
form.append(field, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
186
|
+
}
|
|
187
|
+
async function createMultipartBody(command, maxUploadBytes) {
|
|
188
|
+
const form = new FormData();
|
|
189
|
+
const properties = isRecord(command.action.requestBody?.schema.properties)
|
|
190
|
+
? command.action.requestBody.schema.properties
|
|
191
|
+
: {};
|
|
192
|
+
for (const [field, value] of Object.entries(command.data ?? {})) {
|
|
193
|
+
if (!isBinarySchema(properties[field])) {
|
|
194
|
+
appendTextValue(form, field, value);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const values = Array.isArray(value) ? value : [value];
|
|
198
|
+
if (values.length === 0) {
|
|
199
|
+
throw new ZentaoError('E_INVALID_UPLOAD_SOURCE', { field });
|
|
200
|
+
}
|
|
201
|
+
for (const item of values) {
|
|
202
|
+
const prepared = await prepareFile(item, field, maxUploadBytes);
|
|
203
|
+
form.append(field, prepared.blob, prepared.filename);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return form;
|
|
207
|
+
}
|
|
208
|
+
/** 根据模块动作的媒体类型,把解析后的普通请求体转换为底层客户端可发送的请求体。 */
|
|
209
|
+
export async function prepareActionBody(command, options = {}) {
|
|
210
|
+
const mediaType = command.action.requestBody?.mediaType ?? 'application/json';
|
|
211
|
+
if (mediaType === 'multipart/form-data') {
|
|
212
|
+
return {
|
|
213
|
+
body: await createMultipartBody(command, resolveMaxUploadBytes(options.maxUploadBytes)),
|
|
214
|
+
bodyType: 'raw',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (mediaType === 'application/x-www-form-urlencoded') {
|
|
218
|
+
return { body: command.data, bodyType: 'form' };
|
|
219
|
+
}
|
|
220
|
+
return { body: command.data };
|
|
221
|
+
}
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
package/dist/types/module.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface ModuleActionParam {
|
|
|
27
27
|
defaultValue?: unknown;
|
|
28
28
|
/** 参数值类型,用于基础类型转换。 */
|
|
29
29
|
type?: 'string' | 'number' | 'boolean';
|
|
30
|
+
/** OpenAPI schema format,例如 `binary` 或 `int32`。 */
|
|
31
|
+
format?: string;
|
|
30
32
|
/** 参数可选值。 */
|
|
31
33
|
options?: readonly ModuleActionParamOption[];
|
|
32
34
|
}
|
|
@@ -34,12 +36,16 @@ export interface ModuleActionParam {
|
|
|
34
36
|
export type ModuleActionResultType = 'text' | 'object' | 'list';
|
|
35
37
|
/** 列表分页信息别名。 */
|
|
36
38
|
export type ListPagerInfo = Pager;
|
|
39
|
+
/** 高阶请求支持的请求体媒体类型。 */
|
|
40
|
+
export type ModuleActionRequestMediaType = 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded';
|
|
37
41
|
/** 模块动作请求体定义。 */
|
|
38
42
|
export interface ModuleActionRequestBody {
|
|
39
43
|
/** 请求体类型。 */
|
|
40
44
|
type?: 'object' | 'string';
|
|
41
45
|
/** 请求体是否必填。 */
|
|
42
46
|
required?: boolean;
|
|
47
|
+
/** 请求体媒体类型;省略时按 `application/json` 处理。 */
|
|
48
|
+
mediaType?: ModuleActionRequestMediaType;
|
|
43
49
|
/** OpenAPI 风格 schema,用于从 params 组装 body。 */
|
|
44
50
|
schema: Readonly<Record<string, unknown>>;
|
|
45
51
|
/** 请求体示例。 */
|
package/dist/types/options.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface RequestOptions extends ProcessListOptions {
|
|
|
33
33
|
timeout?: number;
|
|
34
34
|
/** 本次调用 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
35
35
|
insecure?: boolean;
|
|
36
|
+
/** 单个本地文件或 Blob 上传的最大字节数,默认 50 MiB。 */
|
|
37
|
+
maxUploadBytes?: number;
|
|
36
38
|
/** 单条对象转换函数,在字段摘取前执行;列表转换请使用 `convert`。 */
|
|
37
39
|
convertSingle?: ProcessSingleOptions['convert'];
|
|
38
40
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Node.js/Bun 本地文件路径形式的上传输入。 */
|
|
2
|
+
export interface FileUploadPathInput {
|
|
3
|
+
/** 本地文件路径。 */
|
|
4
|
+
path: string;
|
|
5
|
+
/** 上传时使用的文件名;省略时取路径 basename。 */
|
|
6
|
+
filename?: string;
|
|
7
|
+
/** 显式 MIME 类型;省略时按文件名推断。 */
|
|
8
|
+
contentType?: string;
|
|
9
|
+
}
|
|
10
|
+
/** 内存数据形式的上传输入。 */
|
|
11
|
+
export interface FileUploadDataInput {
|
|
12
|
+
/** 文件内容。 */
|
|
13
|
+
data: Blob | ArrayBuffer | ArrayBufferView;
|
|
14
|
+
/** 上传时使用的文件名。 */
|
|
15
|
+
filename: string;
|
|
16
|
+
/** 显式 MIME 类型;省略时优先使用 Blob.type,再按文件名推断。 */
|
|
17
|
+
contentType?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 高阶 `request()` 可接受的文件输入。
|
|
21
|
+
*
|
|
22
|
+
* - Node.js/Bun:可直接传本地路径或 {@link FileUploadPathInput}。
|
|
23
|
+
* - 浏览器:传 `File`(属于 `Blob`)、`Blob` 或 {@link FileUploadDataInput};
|
|
24
|
+
* 无文件名的 `Blob` 会按 MIME 类型生成文件名,需要精确文件名时使用数据对象形式。
|
|
25
|
+
*/
|
|
26
|
+
export type FileUploadSource = string | Blob | FileUploadPathInput | FileUploadDataInput;
|
|
27
|
+
/** `request("file/create")` 的参数。 */
|
|
28
|
+
export type FileCreateParams = Record<string, unknown> & {
|
|
29
|
+
file: FileUploadSource;
|
|
30
|
+
objectType: 'bug' | 'story' | 'task' | 'testcase' | (string & {});
|
|
31
|
+
objectID: number;
|
|
32
|
+
};
|
|
33
|
+
/** `request("file/create")` 归一化后的结果。 */
|
|
34
|
+
export interface FileCreateResult {
|
|
35
|
+
id: number;
|
|
36
|
+
url?: string;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/version.js
CHANGED