snan-cli 1.0.1 → 1.0.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 +19 -0
- package/package.json +3 -3
- package/src/index.js +42 -26
- package/src/modules/asset.js +15 -1
- package/src/modules/org.js +65 -0
package/README.md
CHANGED
|
@@ -67,6 +67,12 @@ astro metadata tableDetail --fqn=tenant-doris_prod.default.prod_dwi.dwi_order
|
|
|
67
67
|
### 数据资产
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
+
# 查询 L3 业务模型;结果包含所属业务域、业务阶段及关联资产概要
|
|
71
|
+
astro asset businessModels --keyword="销售|订单"
|
|
72
|
+
|
|
73
|
+
# name 必须来自 businessModels 返回结果
|
|
74
|
+
astro asset businessModelDetail --name="销售订单处理"
|
|
75
|
+
|
|
70
76
|
astro asset dataModels --keyword="销售|订单"
|
|
71
77
|
astro asset dataModelDetail --nameEn=SalesOrder
|
|
72
78
|
astro asset indicators --keyword="GMV|成交额"
|
|
@@ -91,6 +97,19 @@ astro data queryOrigin --body="{\"dsCode\":\"erp_prod\",\"sql\":\"SELECT order_n
|
|
|
91
97
|
|
|
92
98
|
`dsCode` 必须来自 `metadata sources`,并且对应 `sourceType=DATASOURCE` 的来源。
|
|
93
99
|
|
|
100
|
+
### 组织查询
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# 获取部门清单
|
|
104
|
+
astro org departments --raw
|
|
105
|
+
|
|
106
|
+
# 按关键字、部门筛选人员
|
|
107
|
+
astro org users --keyword '张三' --dept-code D001 --page 1 --page-size 100 --raw
|
|
108
|
+
|
|
109
|
+
# 按角色名称筛选角色
|
|
110
|
+
astro org roles --keyword '销售' --page 1 --page-size 1000 --raw
|
|
111
|
+
```
|
|
112
|
+
|
|
94
113
|
### 数据标准
|
|
95
114
|
|
|
96
115
|
```bash
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snan-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "CLI tool to expose backend HTTP interfaces as command-line commands",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
|
-
"astro": "
|
|
7
|
+
"astro": "bin/astro"
|
|
8
8
|
},
|
|
9
9
|
"main": "src/index.js",
|
|
10
10
|
"files": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node bin/astro",
|
|
18
|
-
"test": "node test
|
|
18
|
+
"test": "node --test"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"cli",
|
package/src/index.js
CHANGED
|
@@ -34,6 +34,15 @@ function setupCompletion(modules) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function parseOptionValue(param, value) {
|
|
38
|
+
if (param.type !== 'integer') return value;
|
|
39
|
+
const parsed = Number.parseInt(value, 10);
|
|
40
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
41
|
+
throw new Error(`--${param.flag || param.name} must be a positive integer`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
async function main() {
|
|
38
47
|
const modules = loadModules();
|
|
39
48
|
|
|
@@ -86,13 +95,15 @@ async function main() {
|
|
|
86
95
|
|
|
87
96
|
const method = (actionDef.method || 'POST').toUpperCase();
|
|
88
97
|
const paramDefs = actionDef.params || [];
|
|
98
|
+
const bodyParam = method === 'GET' ? null : paramDefs.find(p => p.name === 'body');
|
|
99
|
+
const optionParamDefs = bodyParam
|
|
100
|
+
? paramDefs.filter(p => p.name !== 'body')
|
|
101
|
+
: paramDefs;
|
|
89
102
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
} else {
|
|
95
|
-
const bodyParam = paramDefs.find(p => p.name === 'body');
|
|
103
|
+
optionParamDefs.forEach(p => {
|
|
104
|
+
cmd.option(`--${p.flag || p.name} <value>`, p.description);
|
|
105
|
+
});
|
|
106
|
+
if (method !== 'GET') {
|
|
96
107
|
const bodyDescription = bodyParam?.description || 'JSON body for the request';
|
|
97
108
|
if (bodyParam?.required) {
|
|
98
109
|
cmd.requiredOption('--body <json>', bodyDescription);
|
|
@@ -107,35 +118,38 @@ async function main() {
|
|
|
107
118
|
try {
|
|
108
119
|
// action.path 优先(支持子路径),否则自动拼接 /actionName
|
|
109
120
|
const url = actionDef.path ? `${urlPrefix}${actionDef.path}` : `${urlPrefix}/${actionName}`;
|
|
110
|
-
let requestParams = {};
|
|
111
|
-
|
|
112
|
-
if (method
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
} else {
|
|
122
|
-
if (options.body) {
|
|
123
|
-
try {
|
|
124
|
-
requestParams = JSON.parse(options.body);
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.error(`Error: Invalid JSON in --body: ${err.message}`);
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
121
|
+
let requestParams = { ...(actionDef.requestDefaults || {}) };
|
|
122
|
+
|
|
123
|
+
if (method !== 'GET' && options.body) {
|
|
124
|
+
try {
|
|
125
|
+
requestParams = { ...requestParams, ...JSON.parse(options.body) };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`Error: Invalid JSON in --body: ${err.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
129
|
}
|
|
130
|
+
} else if (bodyParam && bodyParam.required) {
|
|
131
|
+
throw new Error('--body is required');
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
optionParamDefs.forEach(p => {
|
|
135
|
+
if (options[p.name] !== undefined) {
|
|
136
|
+
requestParams[p.requestName || p.name] = parseOptionValue(p, options[p.name]);
|
|
137
|
+
} else if (p.required) {
|
|
138
|
+
throw new Error(`--${p.flag || p.name} is required`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let response = await httpClient.request2(method, url, requestParams);
|
|
133
143
|
|
|
134
144
|
if (response && response.isHttpError) {
|
|
135
145
|
printError(response);
|
|
136
146
|
process.exit(1);
|
|
137
147
|
}
|
|
138
148
|
|
|
149
|
+
if (actionDef.transformResponse) {
|
|
150
|
+
response = actionDef.transformResponse(response);
|
|
151
|
+
}
|
|
152
|
+
|
|
139
153
|
print(response, { raw: options.raw });
|
|
140
154
|
} catch (err) {
|
|
141
155
|
printError(err);
|
|
@@ -152,3 +166,5 @@ main().catch(err => {
|
|
|
152
166
|
console.error('Fatal error:', err.message);
|
|
153
167
|
process.exit(1);
|
|
154
168
|
});
|
|
169
|
+
|
|
170
|
+
module.exports = { parseOptionValue };
|
package/src/modules/asset.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
name: 'asset',
|
|
3
|
-
description: '
|
|
3
|
+
description: '数据资产:业务模型、数据模型和指标查询',
|
|
4
4
|
urlPrefix: '/astro-server/openapi/v1/asset',
|
|
5
5
|
actions: {
|
|
6
|
+
businessModels: {
|
|
7
|
+
description: '获取 L3 业务模型列表(含所属业务域、业务阶段及关联资产概要)',
|
|
8
|
+
method: 'GET',
|
|
9
|
+
params: [
|
|
10
|
+
{ name: 'keyword', description: '查询关键字,多个关键字用空格或|分隔,为空查询全部', required: false },
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
businessModelDetail: {
|
|
14
|
+
description: '根据业务模型名称获取详情(含业务定义、层级及关联资产)',
|
|
15
|
+
method: 'GET',
|
|
16
|
+
params: [
|
|
17
|
+
{ name: 'name', description: '业务模型名称(唯一标识),必须来自 businessModels 返回结果', required: true },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
6
20
|
dataModels: {
|
|
7
21
|
description: '获取数据资产模型列表(含可用表信息)',
|
|
8
22
|
method: 'GET',
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function mapData(response, mapper) {
|
|
2
|
+
if (!response || response.data === undefined) return response;
|
|
3
|
+
return { ...response, data: mapper(response.data) };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function roleSummary(role) {
|
|
7
|
+
return {
|
|
8
|
+
code: role && role.code,
|
|
9
|
+
roleName: role && role.roleName,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
name: 'org',
|
|
15
|
+
description: '组织查询:部门、人员和角色只读清单',
|
|
16
|
+
urlPrefix: '/astro-server/openapi/v1/org',
|
|
17
|
+
actions: {
|
|
18
|
+
departments: {
|
|
19
|
+
description: '获取当前租户的部门清单',
|
|
20
|
+
method: 'GET',
|
|
21
|
+
path: '/departments',
|
|
22
|
+
transformResponse: response => mapData(response, data => (data || []).map(item => ({
|
|
23
|
+
code: item.code,
|
|
24
|
+
name: item.name,
|
|
25
|
+
}))),
|
|
26
|
+
},
|
|
27
|
+
users: {
|
|
28
|
+
description: '获取当前租户的人员清单',
|
|
29
|
+
method: 'GET',
|
|
30
|
+
path: '/users',
|
|
31
|
+
requestDefaults: { pageNo: 1, pageSize: 100 },
|
|
32
|
+
params: [
|
|
33
|
+
{ name: 'keyword', description: '姓名或账号关键字' },
|
|
34
|
+
{ name: 'deptCode', flag: 'dept-code', description: '部门编码' },
|
|
35
|
+
{ name: 'page', requestName: 'pageNo', flag: 'page', description: '页码,默认 1', type: 'integer' },
|
|
36
|
+
{ name: 'pageSize', flag: 'page-size', description: '每页数量,默认 100', type: 'integer' },
|
|
37
|
+
],
|
|
38
|
+
transformResponse: response => mapData(response, data => ({
|
|
39
|
+
...data,
|
|
40
|
+
result: (data && data.result ? data.result : []).map(user => ({
|
|
41
|
+
account: user.account,
|
|
42
|
+
name: user.name,
|
|
43
|
+
deptCode: user.deptCode,
|
|
44
|
+
deptName: user.deptName,
|
|
45
|
+
roles: (user.roles || []).map(roleSummary),
|
|
46
|
+
})),
|
|
47
|
+
})),
|
|
48
|
+
},
|
|
49
|
+
roles: {
|
|
50
|
+
description: '获取当前租户的角色清单',
|
|
51
|
+
method: 'GET',
|
|
52
|
+
path: '/roles',
|
|
53
|
+
requestDefaults: { pageNo: 1, pageSize: 1000 },
|
|
54
|
+
params: [
|
|
55
|
+
{ name: 'keyword', description: '角色名称关键字' },
|
|
56
|
+
{ name: 'page', requestName: 'pageNo', flag: 'page', description: '页码,默认 1', type: 'integer' },
|
|
57
|
+
{ name: 'pageSize', flag: 'page-size', description: '每页数量,默认 1000', type: 'integer' },
|
|
58
|
+
],
|
|
59
|
+
transformResponse: response => mapData(response, data => ({
|
|
60
|
+
...data,
|
|
61
|
+
result: (data && data.result ? data.result : []).map(roleSummary),
|
|
62
|
+
})),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|